iagofp commited on
Commit
7eadb90
·
1 Parent(s): 0f3d57b

Separacion codigo e documentacion

Browse files
Files changed (40) hide show
  1. DIAGRAMAS_PATRONES.md +339 -0
  2. README.md +261 -0
  3. backend/aplicacion.py +124 -22
  4. backend/base_datos.py +4 -7
  5. backend/conexion_bd.py +0 -3
  6. backend/dao/ciclo_dao.py +1 -2
  7. backend/dao/emocion_dao.py +1 -2
  8. backend/dao/historial_dao.py +2 -2
  9. backend/dao/pelicula_dao.py +14 -22
  10. backend/dao/usuario_dao.py +4 -5
  11. backend/do.py +42 -0
  12. backend/{modelos.py → modelos_servicios.py} +1 -97
  13. backend/scripts/crear_bd.py +1 -4
  14. backend/scripts/limpiar_bdm.py +1 -4
  15. backend/services/calculos.py +7 -2
  16. backend/services/estrategias_recomendacion.py +1 -1
  17. backend/services/movielens_service.py +59 -0
  18. backend/services/pipeline.py +2 -1
  19. backend/services/recomendacion.py +1 -97
  20. backend/vo.py +34 -0
  21. chatbot/src/views/AuthView.vue +412 -135
  22. {notebooks/data/raw → data}/ml-latest-small/links.csv +0 -0
  23. {notebooks/data/raw → data}/ml-latest-small/movies.csv +0 -0
  24. {notebooks/data/raw → data}/ml-latest-small/ratings.csv +0 -0
  25. {notebooks/data/raw → data}/ml-latest-small/tags.csv +0 -0
  26. data/ml-latest/README.txt +0 -179
  27. data/{download_movielens_large.py → movielens.py} +4 -4
  28. data/procesado/peliculas_100_emociones.csv +0 -101
  29. data/procesado/peliculas_conocidas.csv +0 -21
  30. data/script.py +0 -174
  31. data/textos_complejo.csv +0 -43
  32. data/textos_simple.csv +0 -43
  33. docs/diagrama_er.md +9 -15
  34. docs/login_seq.md +28 -0
  35. notebooks/01_exploracion_apis.ipynb +0 -0
  36. notebooks/02_exploracion_datasets.ipynb +0 -0
  37. notebooks/data/raw/ml-latest-small/README.txt +0 -153
  38. notebooks/data/raw/tmdb5k/tmdb-movie-metadata/tmdb_5000_movies.csv +0 -0
  39. readme.txt +0 -46
  40. requirements.txt +2 -0
DIAGRAMAS_PATRONES.md ADDED
@@ -0,0 +1,339 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Diagramas de Patrones de Diseño - ValorSentimental
2
+
3
+ ## 1. Patrón Singleton: ConexionBD
4
+
5
+ ```mermaid
6
+ classDiagram
7
+ class ConexionBD {
8
+ -instancia ConexionBD
9
+ -db_path str
10
+ +new(cls) ConexionBD
11
+ +instancia() ConexionBD
12
+ +obtener_conexion() Connection
13
+ }
14
+
15
+ note for ConexionBD "Singleton clásico que garantiza una única instancia de conexión a la base de datos SQLite"
16
+ ```
17
+
18
+ ### Explicación del Singleton:
19
+
20
+ - **`_instancia`**: Variable de clase que almacena la única instancia (inicialmente `None`)
21
+ - **`__new__()`**: Método que controla la creación de objetos:
22
+ - Si `_instancia` es `None`, crea la primera instancia
23
+ - Si ya existe, devuelve la instancia existente
24
+ - **`instancia()`**: Método de conveniencia para obtener la instancia
25
+ - **Beneficio**: Garantiza que toda la aplicación comparta una única conexión a la BD
26
+
27
+ ---
28
+
29
+ ## 2. Patrón Factory + DAO + DO/VO
30
+
31
+ ```mermaid
32
+ classDiagram
33
+ class Usuario {
34
+ +id str
35
+ +username str
36
+ +token str
37
+ }
38
+
39
+ class Pelicula {
40
+ +id str
41
+ +titulo str
42
+ +genero str
43
+ }
44
+
45
+ class Emocion {
46
+ +id int
47
+ +user_id str
48
+ +texto_analizado str
49
+ +emocion str
50
+ +valencia str
51
+ +analizado_en str
52
+ }
53
+
54
+ class HistorialPelicula {
55
+ +id int
56
+ +user_id str
57
+ +pelicula_id str
58
+ +emocion_id int
59
+ +valoracion float
60
+ +texto_sesion str
61
+ +visto_en str
62
+ }
63
+
64
+ class CicloRecomendacion {
65
+ +id int
66
+ +user_id str
67
+ +emocion_pre_id int
68
+ +estrategia int
69
+ +creado_en str
70
+ +pelicula_id str
71
+ +emocion_post_id int
72
+ }
73
+
74
+ class EmocionVO {
75
+ +id int
76
+ +emocion str
77
+ +valencia str
78
+ +analizado_en str
79
+ +texto_analizado str
80
+ +desde(e) EmocionVO
81
+ }
82
+
83
+ class PeliculaVistaVO {
84
+ +id int
85
+ +user_id str
86
+ +movie_id str
87
+ +titulo str
88
+ +emocion str
89
+ +valoracion float
90
+ +texto_sesion str
91
+ +visto_en str
92
+ }
93
+
94
+ class UsuarioDao {
95
+ -bd ConexionBD
96
+ +obtener_conexion() Connection
97
+ +registrar(nombre, contrasena) Usuario
98
+ +login(nombre, contrasena) Usuario
99
+ +obtener_por_id(user_id) Usuario
100
+ +obtener_por_nombre(nombre) Usuario
101
+ +obtener_por_token(token) Usuario
102
+ +actualizar_token(user_id, token) bool
103
+ +cerrar_sesion(token) bool
104
+ +actualizar_contrasena(user_id, contrasena_nueva) bool
105
+ +eliminar(user_id) bool
106
+ }
107
+
108
+ class PeliculaDao {
109
+ -bd ConexionBD
110
+ +obtener_conexion() Connection
111
+ }
112
+
113
+ class EmocionDao {
114
+ -bd ConexionBD
115
+ +obtener_conexion() Connection
116
+ }
117
+
118
+ class HistorialDao {
119
+ -bd ConexionBD
120
+ +obtener_conexion() Connection
121
+ }
122
+
123
+ class CicloDao {
124
+ -bd ConexionBD
125
+ +obtener_conexion() Connection
126
+ }
127
+
128
+ class ConexionBD {
129
+ -instancia ConexionBD
130
+ -db_path str
131
+ +new(cls) ConexionBD
132
+ +instancia() ConexionBD
133
+ +obtener_conexion() Connection
134
+ }
135
+
136
+ EmocionVO "1" <-- "1" Emocion : convierte desde
137
+ PeliculaVistaVO "1" --> "1" HistorialPelicula : basado en
138
+ PeliculaVistaVO "1" --> "1" Pelicula : contiene datos de
139
+
140
+ UsuarioDao "1" --> "1" ConexionBD : usa Singleton
141
+ PeliculaDao "1" --> "1" ConexionBD : usa Singleton
142
+ EmocionDao "1" --> "1" ConexionBD : usa Singleton
143
+ HistorialDao "1" --> "1" ConexionBD : usa Singleton
144
+ CicloDao "1" --> "1" ConexionBD : usa Singleton
145
+
146
+ UsuarioDao "1" --> "*" Usuario : gestiona CRUD
147
+ PeliculaDao "1" --> "*" Pelicula : gestiona CRUD
148
+ EmocionDao "1" --> "*" Emocion : gestiona CRUD
149
+ HistorialDao "1" --> "*" HistorialPelicula : gestiona CRUD
150
+ CicloDao "1" --> "*" CicloRecomendacion : gestiona CRUD
151
+ ```
152
+
153
+ ---
154
+
155
+ ## 3. Diagrama de Capas (Arquitectura)
156
+
157
+ ```mermaid
158
+ graph TB
159
+ subgraph Presentacion["CAPA PRESENTACION"]
160
+ API["REST API / Vue.js"]
161
+ end
162
+
163
+ subgraph Servicios["CAPA SERVICIOS"]
164
+ Recomendacion["RecomendacionService"]
165
+ Pipeline["PipelineService"]
166
+ Analisis["AnalisisSentimientosService"]
167
+ Calculos["CalculosService"]
168
+ end
169
+
170
+ subgraph AccesoDatos["CAPA ACCESO A DATOS (DAOs)"]
171
+ UsuarioDAO["UsuarioDao"]
172
+ PeliculaDAO["PeliculaDao"]
173
+ EmocionDAO["EmocionDao"]
174
+ HistorialDAO["HistorialDao"]
175
+ CicloDAO["CicloDao"]
176
+ end
177
+
178
+ subgraph Objetos["CAPA OBJETOS (DOs y VOs)"]
179
+ DO["Domain Objects<br/>Usuario, Pelicula, Emocion<br/>HistorialPelicula, CicloRecomendacion"]
180
+ VO["Value Objects<br/>EmocionVO, PeliculaVistaVO"]
181
+ end
182
+
183
+ subgraph BaseDatos["CAPA BASE DE DATOS"]
184
+ Singleton["ConexionBD<br/>Singleton"]
185
+ SQLite["SQLite<br/>history.db"]
186
+ end
187
+
188
+ API --> Recomendacion
189
+ API --> Pipeline
190
+ API --> Analisis
191
+
192
+ Recomendacion --> UsuarioDAO
193
+ Recomendacion --> PeliculaDAO
194
+ Recomendacion --> EmocionDAO
195
+ Recomendacion --> CicloDAO
196
+
197
+ Pipeline --> EmocionDAO
198
+ Pipeline --> HistorialDAO
199
+
200
+ Analisis --> EmocionDAO
201
+
202
+ UsuarioDAO --> DO
203
+ PeliculaDAO --> DO
204
+ EmocionDAO --> DO
205
+ HistorialDAO --> DO
206
+ CicloDAO --> DO
207
+
208
+ DO --> VO
209
+
210
+ UsuarioDAO --> Singleton
211
+ PeliculaDAO --> Singleton
212
+ EmocionDAO --> Singleton
213
+ HistorialDAO --> Singleton
214
+ CicloDAO --> Singleton
215
+
216
+ Singleton --> SQLite
217
+ ```
218
+
219
+ ---
220
+
221
+ ## 4. Patrón Factory (implícito en DAOs)
222
+
223
+ ```mermaid
224
+ classDiagram
225
+ class DaoFactory {
226
+ -usuario_dao UsuarioDao
227
+ -pelicula_dao PeliculaDao
228
+ -emocion_dao EmocionDao
229
+ -historial_dao HistorialDao
230
+ -ciclo_dao CicloDao
231
+ +obtener_usuario_dao() UsuarioDao
232
+ +obtener_pelicula_dao() PeliculaDao
233
+ +obtener_emocion_dao() EmocionDao
234
+ +obtener_historial_dao() HistorialDao
235
+ +obtener_ciclo_dao() CicloDao
236
+ }
237
+
238
+ class UsuarioDao {
239
+ -bd ConexionBD
240
+ +obtener_conexion() Connection
241
+ }
242
+
243
+ class PeliculaDao {
244
+ -bd ConexionBD
245
+ +obtener_conexion() Connection
246
+ }
247
+
248
+ class EmocionDao {
249
+ -bd ConexionBD
250
+ +obtener_conexion() Connection
251
+ }
252
+
253
+ class HistorialDao {
254
+ -bd ConexionBD
255
+ +obtener_conexion() Connection
256
+ }
257
+
258
+ class CicloDao {
259
+ -bd ConexionBD
260
+ +obtener_conexion() Connection
261
+ }
262
+
263
+ DaoFactory --> UsuarioDao : crea/devuelve
264
+ DaoFactory --> PeliculaDao : crea/devuelve
265
+ DaoFactory --> EmocionDao : crea/devuelve
266
+ DaoFactory --> HistorialDao : crea/devuelve
267
+ DaoFactory --> CicloDao : crea/devuelve
268
+
269
+ note for DaoFactory "Factory Pattern: proporciona un punto único de acceso a todos los DAOs. Beneficios: centralización, testing y consistencia"
270
+ ```
271
+
272
+ ## 6. Responsabilidades por Capa
273
+
274
+ | Capa | Responsabilidad | Ejemplo |
275
+ |------|-----------------|---------|
276
+ | **DO (Domain Objects)** | Entidades del negocio, sin lógica | `Usuario`, `Pelicula`, `Emocion` |
277
+ | **VO (Value Objects)** | Objetos inmutables para transferencia | `EmocionVO`, `PeliculaVistaVO` |
278
+ | **DAO (Data Access Object)** | Abstracción de acceso a datos | `UsuarioDao.login()`, `EmocionDao.crear()` |
279
+ | **Singleton (ConexionBD)** | Garantiza conexión única a BD | Una única instancia compartida |
280
+ | **Servicios** | Lógica de negocio | `RecomendacionService`, `AnalisisSentimientos` |
281
+
282
+ ---
283
+
284
+ ## 7. Ventajas de esta Arquitectura
285
+
286
+ ### Singleton (ConexionBD)
287
+ ✅ Una única conexión a la BD
288
+ ✅ Gestión centralizada de recursos
289
+ ✅ Thread-safe (si se implementa correctamente)
290
+ ✅ Fácil de testear (se puede mockear)
291
+
292
+ ### DAO Pattern
293
+ ✅ Abstracción del acceso a datos
294
+ ✅ Facilita cambiar de BD sin afectar servicios
295
+ ✅ Centraliza lógica SQL
296
+ ✅ Reutilizable
297
+
298
+ ### DO/VO Pattern
299
+ ✅ Separación clara de responsabilidades
300
+ ✅ VOs inmutables = seguridad en concurrencia
301
+ ✅ DOs para persistencia, VOs para transferencia
302
+ ✅ Facilita serialización a JSON
303
+
304
+ ---
305
+
306
+ ## 8. Recomendaciones de Mejora
307
+
308
+ ### Implementar DaoFactory
309
+ ```python
310
+ class DaoFactory:
311
+ _daos = {}
312
+
313
+ @staticmethod
314
+ def obtener_usuario_dao() -> UsuarioDao:
315
+ if 'usuario' not in DaoFactory._daos:
316
+ DaoFactory._daos['usuario'] = UsuarioDao()
317
+ return DaoFactory._daos['usuario']
318
+ ```
319
+
320
+ ### Agregar Interfaz Base para DAOs
321
+ ```python
322
+ from abc import ABC, abstractmethod
323
+
324
+ class BaseDao(ABC):
325
+ def __init__(self):
326
+ self._bd = ConexionBD.instancia()
327
+
328
+ @abstractmethod
329
+ def obtener_por_id(self, id): pass
330
+
331
+ @abstractmethod
332
+ def crear(self, obj): pass
333
+
334
+ @abstractmethod
335
+ def actualizar(self, obj): pass
336
+
337
+ @abstractmethod
338
+ def eliminar(self, id): pass
339
+ ```
README.md ADDED
@@ -0,0 +1,261 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Valor Sentimental
2
+
3
+ Valor Sentimental es una aplicacion web de recomendacion de peliculas basada en el estado emocional del usuario. El sistema analiza un texto escrito por la persona, identifica su emocion dominante y recomienda peliculas teniendo en cuenta su historial, sus valoraciones y distintas estrategias de exploracion o zona de confort.
4
+
5
+ El proyecto combina un backend en Flask, una interfaz en Vue 3 con Vuetify, una base de datos SQLite local y datos de MovieLens para construir el catalogo de peliculas.
6
+
7
+ ## Caracteristicas principales
8
+
9
+ - Analisis emocional de texto en espanol mediante `pysentimiento/robertuito-emotion-analysis`.
10
+ - Recomendacion de peliculas segun emocion, historial y calidad global.
11
+ - Tres estrategias de recomendacion (`v1`, `v2`, `v3`) implementadas con patron Strategy.
12
+ - Registro, login, logout, cambio de contrasena y eliminacion de cuenta.
13
+ - Historial de peliculas vistas con valoracion de 1 a 5.
14
+ - Seguimiento emocional antes y despues de ver una recomendacion.
15
+ - Onboarding inicial para indicar peliculas ya vistas.
16
+ - Busqueda de peliculas y listado de populares usando MovieLens.
17
+ - Obtencion opcional de posters mediante OMDb.
18
+ - Persistencia local en SQLite.
19
+
20
+ ## Tecnologias
21
+
22
+ ### Backend
23
+
24
+ - Python
25
+ - Flask
26
+ - Flask-CORS
27
+ - SQLite
28
+ - Transformers
29
+ - PyTorch
30
+ - Hugging Face Inference Providers
31
+ - OMDb API opcional
32
+
33
+ ### Frontend
34
+
35
+ - Vue 3
36
+ - Vue Router
37
+ - Vuetify
38
+ - Vite
39
+ - Sass
40
+ - Material Design Icons
41
+
42
+ ### Datos
43
+
44
+ - MovieLens `ml-latest`
45
+
46
+ ## Estructura del proyecto
47
+
48
+ ```text
49
+ ValorSentimental/
50
+ |-- backend/
51
+ | |-- aplicacion.py # Rutas Flask y configuracion principal de la API
52
+ | |-- main.py # Punto de entrada del servidor
53
+ | |-- base_datos.py # Creacion de tablas SQLite
54
+ | |-- conexion_bd.py # Singleton de conexion a BD
55
+ | |-- config.py # Constantes, rutas y variables de entorno
56
+ | |-- dao/ # Acceso a datos
57
+ | `-- services/ # Logica de analisis, recomendacion y chatbot
58
+ |-- chatbot/
59
+ | |-- src/ # Aplicacion Vue
60
+ | |-- package.json
61
+ | `-- vite.config.js
62
+ |-- data/
63
+ | |-- ml-latest/ # CSV de MovieLens
64
+ | |-- procesado/ # Datasets procesados
65
+ | |-- download_movielens_large.py
66
+ | `-- script.py # Generacion del dataset emocional
67
+ |-- docs/ # Diagramas y documentacion auxiliar
68
+ |-- DIAGRAMAS_PATRONES.md # Patrones y arquitectura
69
+ |-- requirements.txt
70
+ `-- README.md
71
+ ```
72
+
73
+ ## Requisitos previos
74
+
75
+ - Python 3.10 o superior recomendado.
76
+ - Node.js 18 o superior recomendado.
77
+ - npm.
78
+ - Conexion a internet en el primer arranque si el modelo de Hugging Face no esta descargado localmente.
79
+
80
+ ## Variables de entorno
81
+
82
+ El backend carga variables desde `backend/.env`.
83
+
84
+ Crea un archivo `backend/.env` con el siguiente contenido si quieres activar las funciones externas:
85
+
86
+ ```env
87
+ HF_TOKEN=tu_token_de_huggingface
88
+ OMDB_API_KEY=tu_api_key_de_omdb
89
+ ```
90
+
91
+ `HF_TOKEN` se usa para generar respuestas de chatbot con Hugging Face. Si falla o no existe, la aplicacion usa una respuesta de respaldo basada en plantilla.
92
+
93
+ `OMDB_API_KEY` es opcional y permite recuperar posters de peliculas desde OMDb.
94
+
95
+ ## Instalacion
96
+
97
+ ### 1. Clonar el repositorio
98
+
99
+ ```bash
100
+ git clone <url-del-repositorio>
101
+ cd ValorSentimental
102
+ ```
103
+
104
+ ### 2. Instalar dependencias del backend
105
+
106
+ ```bash
107
+ python -m venv .venv
108
+ source .venv/bin/activate
109
+ pip install -r requirements.txt
110
+ ```
111
+
112
+ En Windows PowerShell:
113
+
114
+ ```powershell
115
+ python -m venv .venv
116
+ .\.venv\Scripts\Activate.ps1
117
+ pip install -r requirements.txt
118
+ ```
119
+
120
+ ### 3. Instalar dependencias del frontend
121
+
122
+ ```bash
123
+ cd chatbot
124
+ npm install
125
+ ```
126
+
127
+ ## Preparacion de datos
128
+
129
+ El recomendador usa los CSV de MovieLens en `data/ml-latest`. Si no estan presentes, se pueden descargar con:
130
+
131
+ ```bash
132
+ python data/download_movielens_large.py
133
+ ```
134
+
135
+ Tambien se puede generar el dataset procesado de 100 peliculas con emociones inferidas por genero:
136
+
137
+ ```bash
138
+ python data/script.py
139
+ ```
140
+
141
+ El backend intenta cargar primero `data/ml-latest/movies.csv`, `ratings.csv` y `links.csv`. Si no encuentra el dataset completo, usa como alternativa `data/procesado/peliculas_100_emociones.csv`.
142
+
143
+ ## Ejecucion
144
+
145
+ ### Backend
146
+
147
+ Desde la raiz del proyecto:
148
+
149
+ ```bash
150
+ cd backend
151
+ python main.py
152
+ ```
153
+
154
+ La API queda disponible en:
155
+
156
+ ```text
157
+ http://localhost:5000
158
+ ```
159
+
160
+ En el primer arranque puede tardar porque se carga o descarga el modelo de emociones.
161
+
162
+ ### Frontend
163
+
164
+ En otra terminal:
165
+
166
+ ```bash
167
+ cd chatbot
168
+ npm run dev
169
+ ```
170
+
171
+ Vite mostrara la URL local, normalmente:
172
+
173
+ ```text
174
+ http://localhost:5173
175
+ ```
176
+
177
+ El frontend esta configurado para llamar al backend en `http://localhost:5000`.
178
+
179
+ ## Flujo de uso
180
+
181
+ 1. El usuario se registra o inicia sesion.
182
+ 2. Durante el registro puede anadir peliculas ya vistas para construir un historial inicial.
183
+ 3. En el chat escribe como se siente.
184
+ 4. El backend analiza el texto y calcula emocion dominante, valencia y arousal.
185
+ 5. El recomendador propone peliculas segun el historial y la estrategia seleccionada.
186
+ 6. El usuario puede marcar una pelicula como vista y valorarla.
187
+ 7. Despues puede indicar como se siente, cerrando un ciclo pre/post recomendacion.
188
+
189
+ ## Estrategias de recomendacion
190
+
191
+ Las estrategias estan en `backend/services/estrategias_recomendacion.py`.
192
+
193
+ - `v1`: logica binaria. Si la emocion es positiva, recomienda fuera de la zona de confort; si es negativa, recomienda dentro.
194
+ - `v2`: pondera de forma continua el grado de confort y la calidad de la pelicula.
195
+ - `v3`: calcula un objetivo de confort adaptativo usando valencia y arousal.
196
+
197
+ Si el usuario no tiene historial, el sistema recomienda peliculas de calidad global usando una puntuacion bayesiana suavizada.
198
+
199
+ ## API principal
200
+
201
+ ### Autenticacion
202
+
203
+ - `POST /auth/register`
204
+ - `POST /auth/login`
205
+ - `POST /auth/logout`
206
+ - `POST /auth/password`
207
+ - `POST /auth/verify`
208
+ - `DELETE /auth/account`
209
+
210
+ ### Analisis y recomendacion
211
+
212
+ - `POST /analizar`
213
+ - `POST /recomendacion/seguimiento`
214
+
215
+ ### Historial
216
+
217
+ - `POST /historial/visto`
218
+ - `GET /historial`
219
+ - `DELETE /historial`
220
+ - `GET /historial/transiciones`
221
+
222
+ ### Catalogo
223
+
224
+ - `GET /peliculas/populares`
225
+ - `GET /peliculas/buscar?q=<texto>`
226
+ - `POST /onboarding/historial`
227
+ - `GET /poster/<imdb_id>`
228
+
229
+ ## Base de datos
230
+
231
+ La base de datos se crea automaticamente en:
232
+
233
+ ```text
234
+ backend/history.db
235
+ ```
236
+
237
+ Tablas principales:
238
+
239
+ - `Usuarios`
240
+ - `Peliculas`
241
+ - `Emociones`
242
+ - `Historial_Peliculas`
243
+ - `Ciclo_Recomendacion`
244
+
245
+ La arquitectura usa DAOs para separar el acceso a datos de la logica de negocio y un Singleton para centralizar la conexion SQLite.
246
+
247
+ ## Documentacion adicional
248
+
249
+ El repositorio incluye documentacion de apoyo:
250
+
251
+ - `DIAGRAMAS_PATRONES.md`: patrones de diseno y arquitectura por capas.
252
+ - `docs/diagrama_er.md`: diagrama entidad-relacion.
253
+ - `docs/login_seq.md`: secuencia de login.
254
+
255
+ ## Notas
256
+
257
+ - El modelo de emociones se ejecuta localmente mediante Transformers.
258
+ - La generacion textual del chatbot intenta usar Hugging Face y, si no esta disponible, cae a una plantilla local.
259
+ - OMDb solo es necesario para mostrar posters; el recomendador funciona sin esa clave.
260
+ - `history.db` es una base local generada en ejecucion y no deberia versionarse.
261
+
backend/aplicacion.py CHANGED
@@ -1,8 +1,3 @@
1
- """
2
- Configura y expone la app Flask como singleton.
3
- Las rutas son delegadores delgados: validan la entrada, llaman al servicio correspondiente y serializan la respuesta.
4
- """
5
-
6
  import dataclasses
7
  import re
8
  from datetime import datetime, timezone
@@ -18,30 +13,22 @@ from dao.historial_dao import HistorialDAO
18
  from dao.pelicula_dao import PeliculaDAO
19
  from dao.usuario_dao import UsuarioDao
20
  from base_datos import iniciar_historial_usuario
21
- from modelos import PeliculaVistaVO
22
  from services.pipeline import AnalysisService
23
  from services.analisis_sentimientos import analizar_texto, crear_clasificador_emociones
24
- from services.recomendacion import cargar_dataset_movies
25
 
26
 
27
  _poster_cache: dict[str, str | None] = {}
28
  _movies_index: dict[str, dict] = {} # movieId -> row, built after dataset loads
29
 
30
 
31
- def _year_from_title(title: str) -> str | None:
32
- m = re.search(r"\((\d{4})\)\s*$", title or "")
33
- return m.group(1) if m else None
34
-
35
-
36
- def _meta_pelicula(movie_id: str) -> tuple[str | None, str | None]:
37
- """Returns (anio, genero) from the in-memory dataset index."""
38
  row = _movies_index.get(str(movie_id))
39
  if not row:
40
- return None, None
41
  genres_raw = str(row.get("genres", "") or "").strip()
42
- genero = genres_raw if genres_raw and genres_raw != "(no genres listed)" else None
43
- anio = _year_from_title(str(row.get("title", "") or ""))
44
- return anio, genero
45
 
46
  app = Flask(__name__)
47
  CORS(app)
@@ -197,8 +184,7 @@ def seguimiento_recomendacion():
197
  )
198
 
199
  if id_pelicula:
200
- _anio, _genero = _meta_pelicula(id_pelicula)
201
- _pelicula_dao.guardar_si_no_existe(id_pelicula, titulo_pelicula, anio=_anio, genero=_genero)
202
  if emocion_post_obj:
203
  _ciclo_dao.cerrar_ciclo(
204
  ciclo_id=cycle_id,
@@ -252,8 +238,7 @@ def guardar_visto():
252
  if rating_usuario < 1 or rating_usuario > 5:
253
  return jsonify({"error": "rating_usuario debe estar entre 1 y 5"}), 400
254
 
255
- _anio, _genero = _meta_pelicula(id_pelicula)
256
- _pelicula_dao.guardar_si_no_existe(id_pelicula, titulo_pelicula, anio=_anio, genero=_genero)
257
 
258
  emocion_id = None
259
  if emocion_str:
@@ -364,6 +349,123 @@ def obtener_transiciones():
364
  return jsonify({"items": items[:limit], "count": len(items[:limit])})
365
 
366
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
367
  # ------------------------------------------------------------------
368
  # Poster OMDB
369
  # ------------------------------------------------------------------
 
 
 
 
 
 
1
  import dataclasses
2
  import re
3
  from datetime import datetime, timezone
 
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
18
  from services.analisis_sentimientos import analizar_texto, crear_clasificador_emociones
19
+ from services.movielens_service import cargar_dataset_movies
20
 
21
 
22
  _poster_cache: dict[str, str | None] = {}
23
  _movies_index: dict[str, dict] = {} # movieId -> row, built after dataset loads
24
 
25
 
26
+ def _genero_pelicula(movie_id: str) -> str | None:
 
 
 
 
 
 
27
  row = _movies_index.get(str(movie_id))
28
  if not row:
29
+ return None
30
  genres_raw = str(row.get("genres", "") or "").strip()
31
+ return genres_raw if genres_raw and genres_raw != "(no genres listed)" else None
 
 
32
 
33
  app = Flask(__name__)
34
  CORS(app)
 
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,
 
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:
 
349
  return jsonify({"items": items[:limit], "count": len(items[:limit])})
350
 
351
 
352
+ # ------------------------------------------------------------------
353
+ # Catálogo de películas (búsqueda y populares para onboarding)
354
+ # ------------------------------------------------------------------
355
+
356
+ _PRIOR_COUNT = 50 # same as calculos.py
357
+
358
+ def _puntuacion_bayesiana(row: dict, media_global: float) -> float:
359
+ count = int(row.get("rating_count") or 0)
360
+ mean = float(row.get("rating_mean") or 0.0)
361
+ return ((count * mean) + (_PRIOR_COUNT * media_global)) / (count + _PRIOR_COUNT)
362
+
363
+
364
+ @app.route("/peliculas/populares", methods=["GET"])
365
+ def peliculas_populares():
366
+ try:
367
+ limit = max(1, min(int(request.args.get("limit", 20)), 100))
368
+ except ValueError:
369
+ limit = 20
370
+
371
+ scored = [
372
+ {
373
+ "movie_id": str(r.get("movieId", "")).strip(),
374
+ "titulo": str(r.get("title", "")).strip(),
375
+ "genero": str(r.get("genres", "")).strip(),
376
+ "imdb_id": str(r.get("imdb_id", "")).strip(),
377
+ "rating_mean": round(float(r.get("rating_mean") or 0.0), 2),
378
+ "rating_count": int(r.get("rating_count") or 0),
379
+ }
380
+ for r in sorted(
381
+ _movies_df,
382
+ key=lambda r: _puntuacion_bayesiana(r, _media_rating_global),
383
+ reverse=True,
384
+ )[:limit]
385
+ ]
386
+ return jsonify({"items": scored, "count": len(scored)})
387
+
388
+
389
+ @app.route("/peliculas/buscar", methods=["GET"])
390
+ def buscar_peliculas():
391
+ q = str(request.args.get("q", "")).strip().lower()
392
+ if not q or len(q) < 2:
393
+ return jsonify({"error": "El parámetro q debe tener al menos 2 caracteres"}), 400
394
+ try:
395
+ limit = max(1, min(int(request.args.get("limit", 15)), 50))
396
+ except ValueError:
397
+ limit = 15
398
+
399
+ coincidencias = [
400
+ r for r in _movies_df
401
+ if q in str(r.get("title", "")).lower()
402
+ ]
403
+ coincidencias.sort(
404
+ key=lambda r: _puntuacion_bayesiana(r, _media_rating_global),
405
+ reverse=True,
406
+ )
407
+ items = [
408
+ {
409
+ "movie_id": str(r.get("movieId", "")).strip(),
410
+ "titulo": str(r.get("title", "")).strip(),
411
+ "genero": str(r.get("genres", "")).strip(),
412
+ "imdb_id": str(r.get("imdb_id", "")).strip(),
413
+ "rating_mean": round(float(r.get("rating_mean") or 0.0), 2),
414
+ "rating_count": int(r.get("rating_count") or 0),
415
+ }
416
+ for r in coincidencias[:limit]
417
+ ]
418
+ return jsonify({"items": items, "count": len(items)})
419
+
420
+
421
+ @app.route("/onboarding/historial", methods=["POST"])
422
+ def onboarding_historial():
423
+ """Guarda un lote de películas ya vistas durante el onboarding del registro."""
424
+ payload = request.json or {}
425
+ user_id = str(payload.get("user_id", "")).strip()
426
+ token = str(payload.get("token", "")).strip()
427
+ peliculas = payload.get("peliculas", [])
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
435
+
436
+ from datetime import datetime, timezone
437
+ momento = datetime.now(timezone.utc).isoformat()
438
+ guardadas = 0
439
+
440
+ for peli in peliculas:
441
+ movie_id = str(peli.get("movie_id", "")).strip()
442
+ titulo = str(peli.get("titulo", "")).strip()
443
+ if not movie_id:
444
+ continue
445
+ genero = _genero_pelicula(movie_id) or str(peli.get("genero", "")).strip() or None
446
+ rating_raw = peli.get("valoracion")
447
+ valoracion = None
448
+ if rating_raw is not None:
449
+ try:
450
+ v = float(rating_raw)
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,
459
+ valoracion=valoracion,
460
+ texto=None,
461
+ tiempo=momento,
462
+ )
463
+ if entrada:
464
+ guardadas += 1
465
+
466
+ return jsonify({"ok": True, "guardadas": guardadas}), 201
467
+
468
+
469
  # ------------------------------------------------------------------
470
  # Poster OMDB
471
  # ------------------------------------------------------------------
backend/base_datos.py CHANGED
@@ -26,7 +26,6 @@ def iniciar_historial_usuario() -> None:
26
  CREATE TABLE IF NOT EXISTS Usuarios (
27
  id TEXT PRIMARY KEY,
28
  username TEXT UNIQUE NOT NULL,
29
- email TEXT,
30
  password_hash TEXT NOT NULL,
31
  session_token TEXT,
32
  created_at TEXT NOT NULL
@@ -37,18 +36,16 @@ def iniciar_historial_usuario() -> None:
37
  # ------------------------------------------------------------------ #
38
  # Peliculas #
39
  # PK: id (IMDb/OMDB id) #
40
- # FDs: id → titulo, anio, genero, poster_url #
41
  # Entidad independiente — los datos de la película no dependen #
42
  # del usuario ni de la sesión. #
43
  # ------------------------------------------------------------------ #
44
  conn.execute(
45
  """
46
  CREATE TABLE IF NOT EXISTS Peliculas (
47
- id TEXT PRIMARY KEY,
48
- titulo TEXT NOT NULL,
49
- anio TEXT,
50
- genero TEXT,
51
- poster_url TEXT
52
  )
53
  """
54
  )
 
26
  CREATE TABLE IF NOT EXISTS Usuarios (
27
  id TEXT PRIMARY KEY,
28
  username TEXT UNIQUE NOT NULL,
 
29
  password_hash TEXT NOT NULL,
30
  session_token TEXT,
31
  created_at TEXT NOT NULL
 
36
  # ------------------------------------------------------------------ #
37
  # Peliculas #
38
  # PK: id (IMDb/OMDB id) #
39
+ # FDs: id → titulo, genero #
40
  # Entidad independiente — los datos de la película no dependen #
41
  # del usuario ni de la sesión. #
42
  # ------------------------------------------------------------------ #
43
  conn.execute(
44
  """
45
  CREATE TABLE IF NOT EXISTS Peliculas (
46
+ id TEXT PRIMARY KEY,
47
+ titulo TEXT NOT NULL,
48
+ genero TEXT
 
 
49
  )
50
  """
51
  )
backend/conexion_bd.py CHANGED
@@ -2,10 +2,7 @@ import sqlite3
2
 
3
  from config import HISTORY_DB_PATH
4
 
5
-
6
  class ConexionBD:
7
- """Singleton clásico que centraliza el acceso a la conexión SQLite."""
8
-
9
  _instancia: "ConexionBD | None" = None
10
 
11
  def __new__(cls) -> "ConexionBD":
 
2
 
3
  from config import HISTORY_DB_PATH
4
 
 
5
  class ConexionBD:
 
 
6
  _instancia: "ConexionBD | None" = None
7
 
8
  def __new__(cls) -> "ConexionBD":
backend/dao/ciclo_dao.py CHANGED
@@ -1,6 +1,5 @@
1
  from conexion_bd import ConexionBD
2
- from modelos import CicloRecomendacion
3
-
4
 
5
  class CicloDAO:
6
  def __init__(self):
 
1
  from conexion_bd import ConexionBD
2
+ from do import CicloRecomendacion
 
3
 
4
  class CicloDAO:
5
  def __init__(self):
backend/dao/emocion_dao.py CHANGED
@@ -1,6 +1,5 @@
1
  from conexion_bd import ConexionBD
2
- from modelos import Emocion
3
-
4
 
5
  class EmocionDAO:
6
  def __init__(self):
 
1
  from conexion_bd import ConexionBD
2
+ from do import Emocion
 
3
 
4
  class EmocionDAO:
5
  def __init__(self):
backend/dao/historial_dao.py CHANGED
@@ -1,6 +1,6 @@
1
  from conexion_bd import ConexionBD
2
- from modelos import HistorialPelicula, PeliculaVistaVO
3
-
4
 
5
  class HistorialDAO:
6
  def __init__(self):
 
1
  from conexion_bd import ConexionBD
2
+ from do import HistorialPelicula
3
+ from vo import PeliculaVistaVO
4
 
5
  class HistorialDAO:
6
  def __init__(self):
backend/dao/pelicula_dao.py CHANGED
@@ -1,6 +1,5 @@
1
  from conexion_bd import ConexionBD
2
- from modelos import Pelicula
3
-
4
 
5
  class PeliculaDAO:
6
  def __init__(self):
@@ -9,21 +8,19 @@ class PeliculaDAO:
9
  def obtener_conexion(self):
10
  return self._bd.obtener_conexion()
11
 
12
- def guardar_si_no_existe(self, pelicula_id: str, titulo: str, anio: str | None = None,
13
- genero: str | None = None, poster_url: str | None = None) -> Pelicula:
14
  with self.obtener_conexion() as con:
15
  con.execute(
16
- """INSERT INTO Peliculas (id, titulo, anio, genero, poster_url)
17
- VALUES (?, ?, ?, ?, ?)
18
  ON CONFLICT(id) DO UPDATE SET
19
- anio = COALESCE(Peliculas.anio, excluded.anio),
20
- genero = COALESCE(Peliculas.genero, excluded.genero),
21
- poster_url = COALESCE(Peliculas.poster_url, excluded.poster_url)""",
22
- (pelicula_id, titulo, anio, genero, poster_url),
23
  )
24
  con.commit()
25
  row = con.execute(
26
- "SELECT id, titulo, anio, genero, poster_url FROM Peliculas WHERE id = ?",
27
  (pelicula_id,),
28
  ).fetchone()
29
  return self._row_a_pelicula(row)
@@ -31,7 +28,7 @@ class PeliculaDAO:
31
  def obtener_por_id(self, pelicula_id: str) -> Pelicula | None:
32
  with self.obtener_conexion() as con:
33
  row = con.execute(
34
- "SELECT id, titulo, anio, genero, poster_url FROM Peliculas WHERE id = ?",
35
  (pelicula_id,),
36
  ).fetchone()
37
  if not row:
@@ -41,7 +38,7 @@ class PeliculaDAO:
41
  def buscar_por_titulo(self, texto: str, limit: int = 20) -> list[Pelicula]:
42
  with self.obtener_conexion() as con:
43
  rows = con.execute(
44
- """SELECT id, titulo, anio, genero, poster_url FROM Peliculas
45
  WHERE titulo LIKE ?
46
  ORDER BY titulo
47
  LIMIT ?""",
@@ -49,11 +46,9 @@ class PeliculaDAO:
49
  ).fetchall()
50
  return [self._row_a_pelicula(r) for r in rows]
51
 
52
- def actualizar(self, pelicula_id: str, titulo: str | None = None, anio: str | None = None,
53
- genero: str | None = None, poster_url: str | None = None) -> bool:
54
- campos = {k: v for k, v in
55
- {"titulo": titulo, "anio": anio, "genero": genero, "poster_url": poster_url}.items()
56
- if v is not None}
57
  if not campos:
58
  return False
59
  sets = ", ".join(f"{col} = ?" for col in campos)
@@ -67,7 +62,4 @@ class PeliculaDAO:
67
  return False
68
 
69
  def _row_a_pelicula(self, row) -> Pelicula:
70
- return Pelicula(
71
- id=row["id"], titulo=row["titulo"], anio=row["anio"],
72
- genero=row["genero"], poster_url=row["poster_url"],
73
- )
 
1
  from conexion_bd import ConexionBD
2
+ from do import Pelicula
 
3
 
4
  class PeliculaDAO:
5
  def __init__(self):
 
8
  def obtener_conexion(self):
9
  return self._bd.obtener_conexion()
10
 
11
+ def guardar_si_no_existe(self, pelicula_id: str, titulo: str,
12
+ genero: str | None = None) -> Pelicula:
13
  with self.obtener_conexion() as con:
14
  con.execute(
15
+ """INSERT INTO Peliculas (id, titulo, genero)
16
+ VALUES (?, ?, ?)
17
  ON CONFLICT(id) DO UPDATE SET
18
+ genero = COALESCE(Peliculas.genero, excluded.genero)""",
19
+ (pelicula_id, titulo, genero),
 
 
20
  )
21
  con.commit()
22
  row = con.execute(
23
+ "SELECT id, titulo, genero FROM Peliculas WHERE id = ?",
24
  (pelicula_id,),
25
  ).fetchone()
26
  return self._row_a_pelicula(row)
 
28
  def obtener_por_id(self, pelicula_id: str) -> Pelicula | None:
29
  with self.obtener_conexion() as con:
30
  row = con.execute(
31
+ "SELECT id, titulo, genero FROM Peliculas WHERE id = ?",
32
  (pelicula_id,),
33
  ).fetchone()
34
  if not row:
 
38
  def buscar_por_titulo(self, texto: str, limit: int = 20) -> list[Pelicula]:
39
  with self.obtener_conexion() as con:
40
  rows = con.execute(
41
+ """SELECT id, titulo, genero FROM Peliculas
42
  WHERE titulo LIKE ?
43
  ORDER BY titulo
44
  LIMIT ?""",
 
46
  ).fetchall()
47
  return [self._row_a_pelicula(r) for r in rows]
48
 
49
+ def actualizar(self, pelicula_id: str, titulo: str | None = None,
50
+ genero: str | None = None) -> bool:
51
+ campos = {k: v for k, v in {"titulo": titulo, "genero": genero}.items() if v is not None}
 
 
52
  if not campos:
53
  return False
54
  sets = ", ".join(f"{col} = ?" for col in campos)
 
62
  return False
63
 
64
  def _row_a_pelicula(self, row) -> Pelicula:
65
+ return Pelicula(id=row["id"], titulo=row["titulo"], genero=row["genero"])
 
 
 
backend/dao/usuario_dao.py CHANGED
@@ -4,8 +4,7 @@ from datetime import datetime, timezone
4
  from werkzeug.security import check_password_hash, generate_password_hash
5
 
6
  from conexion_bd import ConexionBD
7
- from modelos import Usuario
8
-
9
 
10
  class UsuarioDao:
11
  def __init__(self):
@@ -21,9 +20,9 @@ class UsuarioDao:
21
  try:
22
  with self.obtener_conexion() as con:
23
  con.execute(
24
- """INSERT INTO Usuarios (id, username, email, password_hash, session_token, created_at)
25
- VALUES (?, ?, ?, ?, ?, ?)""",
26
- (user_id, nombre, "", generate_password_hash(contraseña), token, created_at),
27
  )
28
  con.commit()
29
  return Usuario(id=user_id, username=nombre, token=token)
 
4
  from werkzeug.security import check_password_hash, generate_password_hash
5
 
6
  from conexion_bd import ConexionBD
7
+ from do import Usuario
 
8
 
9
  class UsuarioDao:
10
  def __init__(self):
 
20
  try:
21
  with self.obtener_conexion() as con:
22
  con.execute(
23
+ """INSERT INTO Usuarios (id, username, password_hash, session_token, created_at)
24
+ VALUES (?, ?, ?, ?, ?)""",
25
+ (user_id, nombre, generate_password_hash(contraseña), token, created_at),
26
  )
27
  con.commit()
28
  return Usuario(id=user_id, username=nombre, token=token)
backend/do.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import dataclass
2
+
3
+ @dataclass
4
+ class Usuario:
5
+ id: str
6
+ username: str
7
+ token: str
8
+
9
+ @dataclass
10
+ class Pelicula:
11
+ id: str
12
+ titulo: str
13
+ genero: str | None
14
+
15
+ @dataclass
16
+ class Emocion:
17
+ id: int
18
+ user_id: str
19
+ texto_analizado: str | None
20
+ emocion: str
21
+ valencia: str
22
+ analizado_en: str
23
+
24
+ @dataclass
25
+ class HistorialPelicula:
26
+ id: int
27
+ user_id: str
28
+ pelicula_id: str
29
+ emocion_id: int | None
30
+ valoracion: float | None
31
+ texto_sesion: str | None
32
+ visto_en: str
33
+
34
+ @dataclass
35
+ class CicloRecomendacion:
36
+ id: int
37
+ user_id: str
38
+ emocion_pre_id: int
39
+ estrategia: int
40
+ creado_en: str
41
+ pelicula_id: str | None
42
+ emocion_post_id: int | None
backend/{modelos.py → modelos_servicios.py} RENAMED
@@ -1,99 +1,5 @@
1
  from dataclasses import dataclass, field
2
 
3
-
4
- # ---------------------------------------------------------------------------
5
- # Entidades de dominio (mapeadas 1:1 con tablas)
6
- # ---------------------------------------------------------------------------
7
-
8
- @dataclass
9
- class Usuario:
10
- id: str
11
- username: str
12
- token: str
13
-
14
-
15
- @dataclass
16
- class Pelicula:
17
- id: str
18
- titulo: str
19
- anio: str | None
20
- genero: str | None
21
- poster_url: str | None
22
-
23
-
24
- @dataclass
25
- class Emocion:
26
- id: int
27
- user_id: str
28
- texto_analizado: str | None
29
- emocion: str
30
- valencia: str
31
- analizado_en: str
32
-
33
-
34
- @dataclass
35
- class HistorialPelicula:
36
- id: int
37
- user_id: str
38
- pelicula_id: str
39
- emocion_id: int | None
40
- valoracion: float | None
41
- texto_sesion: str | None
42
- visto_en: str
43
-
44
-
45
- @dataclass
46
- class CicloRecomendacion:
47
- id: int
48
- user_id: str
49
- emocion_pre_id: int
50
- estrategia: int
51
- creado_en: str
52
- pelicula_id: str | None
53
- emocion_post_id: int | None
54
-
55
-
56
- # ---------------------------------------------------------------------------
57
- # Value Objects (VOs) — datos de solo lectura que cruzan capas
58
- # ---------------------------------------------------------------------------
59
-
60
- @dataclass(frozen=True)
61
- class EmocionVO:
62
- """Vista plana de una Emocion para transferir entre capas."""
63
- id: int
64
- emocion: str
65
- valencia: str
66
- analizado_en: str
67
- texto_analizado: str | None = None
68
-
69
- @staticmethod
70
- def desde(e: Emocion) -> "EmocionVO":
71
- return EmocionVO(
72
- id=e.id,
73
- emocion=e.emocion,
74
- valencia=e.valencia,
75
- analizado_en=e.analizado_en,
76
- texto_analizado=e.texto_analizado,
77
- )
78
-
79
-
80
- @dataclass(frozen=True)
81
- class PeliculaVistaVO:
82
- """Vista plana de Historial + Pelicula para serializar a JSON."""
83
- id: int
84
- user_id: str
85
- movie_id: str
86
- titulo: str
87
- emocion: str | None
88
- valoracion: float | None
89
- texto_sesion: str | None
90
- visto_en: str
91
-
92
-
93
- # ---------------------------------------------------------------------------
94
- # Modelos internos de servicios
95
- # ---------------------------------------------------------------------------
96
-
97
  @dataclass
98
  class ContextoEmocional:
99
  emocion_es: str
@@ -101,7 +7,6 @@ class ContextoEmocional:
101
  valencia_actual: float
102
  historico_arousal: list[float] = field(default_factory=list)
103
 
104
-
105
  @dataclass
106
  class PerfilUsuario:
107
  peliculas_vistas: set[str] = field(default_factory=set)
@@ -112,7 +17,6 @@ class PerfilUsuario:
112
  ranking_generos: dict[str, int] = field(default_factory=dict)
113
  tiene_historial: bool = False
114
 
115
-
116
  @dataclass
117
  class ResultadoAnalisis:
118
  emociones: list[dict]
@@ -129,4 +33,4 @@ class ResultadoAnalisis:
129
  chatbot_texto: str
130
  chatbot_fuente: str
131
  pelicula_transicion: dict | None
132
- recomendaciones: list[dict]
 
1
  from dataclasses import dataclass, field
2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  @dataclass
4
  class ContextoEmocional:
5
  emocion_es: str
 
7
  valencia_actual: float
8
  historico_arousal: list[float] = field(default_factory=list)
9
 
 
10
  @dataclass
11
  class PerfilUsuario:
12
  peliculas_vistas: set[str] = field(default_factory=set)
 
17
  ranking_generos: dict[str, int] = field(default_factory=dict)
18
  tiene_historial: bool = False
19
 
 
20
  @dataclass
21
  class ResultadoAnalisis:
22
  emociones: list[dict]
 
33
  chatbot_texto: str
34
  chatbot_fuente: str
35
  pelicula_transicion: dict | None
36
+ recomendaciones: list[dict]
backend/scripts/crear_bd.py CHANGED
@@ -7,10 +7,7 @@ import argparse
7
  import sys
8
  from pathlib import Path
9
 
10
- BACKEND_DIR = Path(__file__).resolve().parent.parent
11
- ROOT_DIR = BACKEND_DIR.parent
12
- if str(ROOT_DIR) not in sys.path:
13
- sys.path.insert(0, str(ROOT_DIR))
14
 
15
  from base_datos import iniciar_historial_usuario
16
  from config import HISTORY_DB_PATH
 
7
  import sys
8
  from pathlib import Path
9
 
10
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
 
 
 
11
 
12
  from base_datos import iniciar_historial_usuario
13
  from config import HISTORY_DB_PATH
backend/scripts/limpiar_bdm.py CHANGED
@@ -8,10 +8,7 @@ import sqlite3
8
  import sys
9
  from pathlib import Path
10
 
11
- BACKEND_DIR = Path(__file__).resolve().parent.parent
12
- ROOT_DIR = BACKEND_DIR.parent
13
- if str(ROOT_DIR) not in sys.path:
14
- sys.path.insert(0, str(ROOT_DIR))
15
 
16
  from base_datos import iniciar_historial_usuario
17
  from config import HISTORY_DB_PATH
 
8
  import sys
9
  from pathlib import Path
10
 
11
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
 
 
 
12
 
13
  from base_datos import iniciar_historial_usuario
14
  from config import HISTORY_DB_PATH
backend/services/calculos.py CHANGED
@@ -2,7 +2,7 @@ import random
2
  from collections import Counter
3
 
4
  from config import GLOBAL_PRIOR_COUNT, LIKE_THRESHOLD, POSITIVE_EMOTIONS
5
- from modelos import PerfilUsuario
6
 
7
 
8
  def obtener_generos_pelicula(row: dict) -> set[str]:
@@ -119,7 +119,12 @@ def calcular_pertenencia_zona_confort(peli: dict, zona_confort_ponderada: dict[s
119
  if not generos or not zona_confort_ponderada:
120
  return 0.0
121
  pesos = [zona_confort_ponderada.get(g, 0.0) for g in generos]
122
- return sum(pesos) / len(pesos)
 
 
 
 
 
123
 
124
 
125
  def percentil_desde_cero(arousal_actual: float, historico_arousal: list[float]) -> float:
 
2
  from collections import Counter
3
 
4
  from config import GLOBAL_PRIOR_COUNT, LIKE_THRESHOLD, POSITIVE_EMOTIONS
5
+ from modelos_servicios import PerfilUsuario
6
 
7
 
8
  def obtener_generos_pelicula(row: dict) -> set[str]:
 
119
  if not generos or not zona_confort_ponderada:
120
  return 0.0
121
  pesos = [zona_confort_ponderada.get(g, 0.0) for g in generos]
122
+ coincidentes = [p for p in pesos if p > 0.0]
123
+ if not coincidentes:
124
+ return 0.0
125
+ cobertura = len(coincidentes) / len(generos)
126
+ media_coincidentes = sum(coincidentes) / len(coincidentes)
127
+ return cobertura * media_coincidentes
128
 
129
 
130
  def percentil_desde_cero(arousal_actual: float, historico_arousal: list[float]) -> float:
backend/services/estrategias_recomendacion.py CHANGED
@@ -1,7 +1,7 @@
1
  from abc import ABC, abstractmethod
2
 
3
  from config import POSITIVE_EMOTIONS
4
- from modelos import ContextoEmocional, PerfilUsuario
5
  from services.calculos import (
6
  calcular_pertenencia_zona_confort,
7
  grado_confort_vector,
 
1
  from abc import ABC, abstractmethod
2
 
3
  from config import POSITIVE_EMOTIONS
4
+ from modelos_servicios import ContextoEmocional, PerfilUsuario
5
  from services.calculos import (
6
  calcular_pertenencia_zona_confort,
7
  grado_confort_vector,
backend/services/movielens_service.py ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import csv
2
+ from pathlib import Path
3
+
4
+
5
+ def cargar_dataset_movies() -> tuple[list[dict], float]:
6
+ """
7
+ Carga el dataset de MovieLens desde CSVs.
8
+
9
+ Returns:
10
+ tuple: (movies_df, media_rating_global)
11
+ - movies_df: lista de diccionarios con estructura
12
+ {movieId, title, genres, imdb_id, rating_mean, rating_count}
13
+ - media_rating_global: promedio global de ratings
14
+ """
15
+ base_path = Path(__file__).parent.parent.parent / "data" / "ml-latest-small"
16
+
17
+ movies_path = base_path / "movies.csv"
18
+ ratings_path = base_path / "ratings.csv"
19
+ links_path = base_path / "links.csv"
20
+
21
+ movies = {}
22
+ with open(movies_path, "r", encoding="utf-8") as f:
23
+ reader = csv.DictReader(f)
24
+ for row in reader:
25
+ movies[row["movieId"]] = {
26
+ "movieId": row["movieId"],
27
+ "title": row["title"],
28
+ "genres": row["genres"],
29
+ }
30
+
31
+ imdb_ids = {}
32
+ with open(links_path, "r", encoding="utf-8") as f:
33
+ reader = csv.DictReader(f)
34
+ for row in reader:
35
+ imdb_ids[row["movieId"]] = row["imdbId"]
36
+
37
+ ratings_by_movie = {}
38
+ all_ratings = []
39
+ with open(ratings_path, "r", encoding="utf-8") as f:
40
+ reader = csv.DictReader(f)
41
+ for row in reader:
42
+ movie_id = row["movieId"]
43
+ rating = float(row["rating"])
44
+ all_ratings.append(rating)
45
+
46
+ if movie_id not in ratings_by_movie:
47
+ ratings_by_movie[movie_id] = []
48
+ ratings_by_movie[movie_id].append(rating)
49
+
50
+ for movie_id, movie in movies.items():
51
+ ratings = ratings_by_movie.get(movie_id, [])
52
+ movie["rating_count"] = len(ratings)
53
+ movie["rating_mean"] = sum(ratings) / len(ratings) if ratings else 0.0
54
+ movie["imdb_id"] = imdb_ids.get(movie_id, "")
55
+
56
+ movies_df = list(movies.values())
57
+ media_global = sum(all_ratings) / len(all_ratings) if all_ratings else 0.0
58
+
59
+ return movies_df, media_global
backend/services/pipeline.py CHANGED
@@ -10,7 +10,8 @@ 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 import ContextoEmocional, EmocionVO, ResultadoAnalisis
 
14
  from services.chatbot import generar_texto_chatbot
15
  from services.analisis_sentimientos import (
16
  analizar_texto,
 
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
16
  from services.analisis_sentimientos import (
17
  analizar_texto,
backend/services/recomendacion.py CHANGED
@@ -4,11 +4,8 @@ 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
- import csv
8
-
9
- from config import ROOT_DIR
10
  from dao.historial_dao import HistorialDAO
11
- from modelos import ContextoEmocional
12
  from services.estrategias_recomendacion import EstrategiaFactory
13
  from services.calculos import construir_perfil_usuario, recomendar_calidad_aleatoria
14
 
@@ -19,99 +16,6 @@ def obtener_historial_usuario(user_id: str, limit: int = 200) -> list[dict]:
19
  entradas = _historial_dao.obtener_por_usuario(user_id, limit=limit)
20
  return [{"movie_id": h.pelicula_id, "user_rating": h.valoracion} for h in entradas]
21
 
22
-
23
- # ---------------------------------------------------------------------------
24
- # Carga de datos
25
- # ---------------------------------------------------------------------------
26
-
27
- def _cargar_estadisticas_ratings() -> tuple[dict[str, tuple[float, int]], float]:
28
- ratings_path = ROOT_DIR / "data" / "ml-latest" / "ratings.csv"
29
- if not ratings_path.exists():
30
- return {}, 0.0
31
-
32
- movie_sum_count: dict[str, list[float | int]] = {}
33
- total_sum = 0.0
34
- total_count = 0
35
-
36
- with open(ratings_path, "r", encoding="utf-8", newline="") as f:
37
- for row in csv.DictReader(f):
38
- movie_id = str(row.get("movieId", "")).strip()
39
- if not movie_id:
40
- continue
41
- try:
42
- rating = float(row.get("rating", 0) or 0)
43
- except (TypeError, ValueError):
44
- continue
45
- if movie_id not in movie_sum_count:
46
- movie_sum_count[movie_id] = [0.0, 0]
47
- movie_sum_count[movie_id][0] += rating
48
- movie_sum_count[movie_id][1] += 1
49
- total_sum += rating
50
- total_count += 1
51
-
52
- stats: dict[str, tuple[float, int]] = {
53
- mid: (s / c, int(c))
54
- for mid, (s, c) in movie_sum_count.items()
55
- if c
56
- }
57
- global_mean = (total_sum / total_count) if total_count else 0.0
58
- return stats, global_mean
59
-
60
-
61
- def _cargar_links() -> dict[str, str]:
62
- """Returns {movieId: imdbId} from links.csv (tries full dataset first, then small)."""
63
- candidates = [
64
- ROOT_DIR / "data" / "ml-latest" / "links.csv",
65
- ROOT_DIR / "notebooks" / "data" / "raw" / "ml-latest-small" / "links.csv",
66
- ]
67
- for path in candidates:
68
- if path.exists():
69
- with open(path, "r", encoding="utf-8", newline="") as f:
70
- return {
71
- str(row.get("movieId", "")).strip(): str(row.get("imdbId", "")).strip()
72
- for row in csv.DictReader(f)
73
- if str(row.get("imdbId", "")).strip()
74
- }
75
- return {}
76
-
77
-
78
- def cargar_dataset_movies() -> tuple[list[dict], float]:
79
- rating_stats, global_mean = _cargar_estadisticas_ratings()
80
- links = _cargar_links()
81
- path = ROOT_DIR / "data" / "ml-latest" / "movies.csv"
82
-
83
- if path.exists():
84
- with open(path, "r", encoding="utf-8", newline="") as f:
85
- rows = list(csv.DictReader(f))
86
-
87
- for row in rows:
88
- movie_id = str(row.get("movieId", "")).strip()
89
- mean, count = rating_stats.get(movie_id, (0.0, 0))
90
- row["rating_count"] = int(count)
91
- row["rating_mean"] = float(mean)
92
- row["imdb_id"] = links.get(movie_id, "")
93
-
94
- return rows, global_mean
95
-
96
- fallback_path = ROOT_DIR / "data" / "procesado" / "peliculas_100_emociones.csv"
97
- if fallback_path.exists():
98
- with open(fallback_path, "r", encoding="utf-8", newline="") as f:
99
- rows = list(csv.DictReader(f))
100
- for row in rows:
101
- movie_id = str(row.get("movieId", "")).strip()
102
- row["imdb_id"] = links.get(movie_id, "")
103
- total_w = sum(float(r.get("rating_mean", 0) or 0) * int(r.get("rating_count", 0) or 0) for r in rows)
104
- total_n = sum(int(r.get("rating_count", 0) or 0) for r in rows)
105
- fallback_mean = (total_w / total_n) if total_n else 3.5
106
- return rows, fallback_mean
107
-
108
- return [], global_mean
109
-
110
-
111
- # ---------------------------------------------------------------------------
112
- # Punto de entrada principal
113
- # ---------------------------------------------------------------------------
114
-
115
  def recomendar_peliculas(
116
  contexto: ContextoEmocional,
117
  user_id: str,
 
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
 
 
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(
20
  contexto: ContextoEmocional,
21
  user_id: str,
backend/vo.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import dataclass
2
+
3
+ from do import Emocion
4
+
5
+ @dataclass(frozen=True)
6
+ class EmocionVO:
7
+ """Vista plana de una Emocion para transferir entre capas."""
8
+ id: int
9
+ emocion: str
10
+ valencia: str
11
+ analizado_en: str
12
+ texto_analizado: str | None = None
13
+
14
+ @staticmethod
15
+ def desde(e: Emocion) -> "EmocionVO":
16
+ return EmocionVO(
17
+ id=e.id,
18
+ emocion=e.emocion,
19
+ valencia=e.valencia,
20
+ analizado_en=e.analizado_en,
21
+ texto_analizado=e.texto_analizado,
22
+ )
23
+
24
+ @dataclass(frozen=True)
25
+ class PeliculaVistaVO:
26
+ """Vista plana de Historial + Pelicula para serializar a JSON."""
27
+ id: int
28
+ user_id: str
29
+ movie_id: str
30
+ titulo: str
31
+ emocion: str | None
32
+ valoracion: float | None
33
+ texto_sesion: str | None
34
+ visto_en: str
chatbot/src/views/AuthView.vue CHANGED
@@ -20,132 +20,208 @@
20
  <!-- Card -->
21
  <div class="auth-card-wrap">
22
  <v-card class="auth-card" rounded="xl" elevation="0">
23
- <div class="auth-card-header">
24
- <h2 class="card-title">
25
- {{ activeTab === 'login' ? 'Bienvenido de vuelta' : 'Crear cuenta' }}
26
- </h2>
27
- <p class="card-subtitle">
28
- {{ activeTab === 'login'
29
- ? 'Inicia sesión para acceder al chat'
30
- : 'Regístrate para guardar tu historial' }}
31
- </p>
32
- </div>
33
-
34
- <v-tabs v-model="activeTab" color="primary" density="compact" class="auth-tabs">
35
- <v-tab value="login">Iniciar sesión</v-tab>
36
- <v-tab value="register">Registrarse</v-tab>
37
- </v-tabs>
38
-
39
- <v-divider />
40
-
41
- <div class="form-wrap">
42
- <!-- Login -->
43
- <form v-if="activeTab === 'login'" class="auth-form" @submit.prevent="submitLogin">
44
- <v-text-field
45
- v-model="loginForm.username"
46
- label="Nombre de usuario"
47
- variant="outlined"
48
- density="comfortable"
49
- prepend-inner-icon="mdi-account-outline"
50
- autocomplete="username"
51
- hide-details="auto"
52
- class="mb-4"
53
- />
54
- <v-text-field
55
- v-model="loginForm.password"
56
- label="Contraseña"
57
- :type="showLoginPwd ? 'text' : 'password'"
58
- variant="outlined"
59
- density="comfortable"
60
- prepend-inner-icon="mdi-lock-outline"
61
- :append-inner-icon="showLoginPwd ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
62
- autocomplete="current-password"
63
- hide-details="auto"
64
- class="mb-4"
65
- @click:append-inner="showLoginPwd = !showLoginPwd"
66
- />
67
- <v-alert
68
- v-if="loginError"
69
- type="error"
70
- variant="tonal"
71
- rounded="lg"
72
- density="compact"
73
- class="mb-4"
74
- >
75
- {{ loginError }}
76
- </v-alert>
77
- <v-btn
78
- type="submit"
79
- color="primary"
80
- variant="flat"
81
- block
82
- rounded="lg"
83
- size="large"
84
- :loading="loginLoading"
85
- >
86
- Iniciar sesión
87
- </v-btn>
88
- </form>
89
-
90
- <!-- Register -->
91
- <form v-else class="auth-form" @submit.prevent="submitRegister">
92
- <v-text-field
93
- v-model="registerForm.username"
94
- label="Nombre de usuario"
95
- variant="outlined"
96
- density="comfortable"
97
- prepend-inner-icon="mdi-account-outline"
98
- autocomplete="username"
99
- hide-details="auto"
100
- class="mb-4"
101
- />
102
- <v-text-field
103
- v-model="registerForm.email"
104
- label="Email (opcional)"
105
- type="email"
106
- variant="outlined"
107
- density="comfortable"
108
- prepend-inner-icon="mdi-email-outline"
109
- autocomplete="email"
110
- hide-details="auto"
111
- class="mb-4"
112
- />
113
- <v-text-field
114
- v-model="registerForm.password"
115
- label="Contraseña"
116
- :type="showRegPwd ? 'text' : 'password'"
117
- variant="outlined"
118
- density="comfortable"
119
- prepend-inner-icon="mdi-lock-outline"
120
- :append-inner-icon="showRegPwd ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
121
- autocomplete="new-password"
122
- hide-details="auto"
123
- class="mb-4"
124
- @click:append-inner="showRegPwd = !showRegPwd"
125
- />
126
- <v-alert
127
- v-if="registerError"
128
- type="error"
129
- variant="tonal"
130
- rounded="lg"
131
- density="compact"
132
- class="mb-4"
133
- >
134
- {{ registerError }}
135
- </v-alert>
136
- <v-btn
137
- type="submit"
138
- color="primary"
139
- variant="flat"
140
- block
141
- rounded="lg"
142
- size="large"
143
- :loading="registerLoading"
144
- >
145
- Crear cuenta
146
- </v-btn>
147
- </form>
148
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  </v-card>
150
  </div>
151
  </div>
@@ -153,22 +229,37 @@
153
  </template>
154
 
155
  <script setup>
156
- import { ref } from "vue";
157
  import { useRouter } from "vue-router";
158
 
159
  const router = useRouter();
160
  const activeTab = ref("login");
 
161
 
 
162
  const loginForm = ref({ username: "", password: "" });
163
  const loginError = ref("");
164
  const loginLoading = ref(false);
165
  const showLoginPwd = ref(false);
166
 
167
- const registerForm = ref({ username: "", email: "", password: "" });
 
168
  const registerError = ref("");
169
  const registerLoading = ref(false);
170
  const showRegPwd = ref(false);
171
 
 
 
 
 
 
 
 
 
 
 
 
 
172
  async function submitLogin() {
173
  loginError.value = "";
174
  if (!loginForm.value.username || !loginForm.value.password) {
@@ -214,24 +305,110 @@ async function submitRegister() {
214
  const res = await fetch("http://localhost:5000/auth/register", {
215
  method: "POST",
216
  headers: { "Content-Type": "application/json" },
217
- body: JSON.stringify({
218
- username: registerForm.value.username,
219
- email: registerForm.value.email,
220
- password: registerForm.value.password,
221
- }),
222
  });
223
  const data = await res.json();
224
  if (!res.ok) { registerError.value = data.error || "Error al crear la cuenta."; return; }
 
225
  localStorage.setItem("vs_token", data.token);
226
  localStorage.setItem("vs_user_id", data.user_id);
227
  localStorage.setItem("vs_username", data.username);
228
- router.push("/chat");
 
 
 
229
  } catch {
230
  registerError.value = "Error de conexión con el servidor.";
231
  } finally {
232
  registerLoading.value = false;
233
  }
234
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
235
  </script>
236
 
237
  <style scoped>
@@ -276,7 +453,7 @@ async function submitRegister() {
276
  position: relative;
277
  z-index: 1;
278
  width: 100%;
279
- max-width: 440px;
280
  padding: 24px 16px;
281
  margin: 0 auto;
282
  display: flex;
@@ -367,8 +544,108 @@ async function submitRegister() {
367
 
368
  .auth-form { display: flex; flex-direction: column; }
369
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
370
  @media (max-width: 480px) {
371
  .auth-card-header { padding: 20px 20px 12px; }
372
  .form-wrap { padding: 20px 20px 24px; }
 
373
  }
374
  </style>
 
20
  <!-- Card -->
21
  <div class="auth-card-wrap">
22
  <v-card class="auth-card" rounded="xl" elevation="0">
23
+
24
+ <!-- ── Paso 1: Login / Register ───────────────────────── -->
25
+ <template v-if="step === 1">
26
+ <div class="auth-card-header">
27
+ <h2 class="card-title">
28
+ {{ activeTab === 'login' ? 'Bienvenido de vuelta' : 'Crear cuenta' }}
29
+ </h2>
30
+ <p class="card-subtitle">
31
+ {{ activeTab === 'login'
32
+ ? 'Inicia sesión para acceder al chat'
33
+ : 'Regístrate para guardar tu historial' }}
34
+ </p>
35
+ </div>
36
+
37
+ <v-tabs v-model="activeTab" color="primary" density="compact" class="auth-tabs">
38
+ <v-tab value="login">Iniciar sesión</v-tab>
39
+ <v-tab value="register">Registrarse</v-tab>
40
+ </v-tabs>
41
+
42
+ <v-divider />
43
+
44
+ <div class="form-wrap">
45
+ <!-- Login -->
46
+ <form v-if="activeTab === 'login'" class="auth-form" @submit.prevent="submitLogin">
47
+ <v-text-field
48
+ v-model="loginForm.username"
49
+ label="Nombre de usuario"
50
+ variant="outlined"
51
+ density="comfortable"
52
+ prepend-inner-icon="mdi-account-outline"
53
+ autocomplete="username"
54
+ hide-details="auto"
55
+ class="mb-4"
56
+ />
57
+ <v-text-field
58
+ v-model="loginForm.password"
59
+ label="Contraseña"
60
+ :type="showLoginPwd ? 'text' : 'password'"
61
+ variant="outlined"
62
+ density="comfortable"
63
+ prepend-inner-icon="mdi-lock-outline"
64
+ :append-inner-icon="showLoginPwd ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
65
+ autocomplete="current-password"
66
+ hide-details="auto"
67
+ class="mb-4"
68
+ @click:append-inner="showLoginPwd = !showLoginPwd"
69
+ />
70
+ <v-alert v-if="loginError" type="error" variant="tonal" rounded="lg" density="compact" class="mb-4">
71
+ {{ loginError }}
72
+ </v-alert>
73
+ <v-btn type="submit" color="primary" variant="flat" block rounded="lg" size="large" :loading="loginLoading">
74
+ Iniciar sesión
75
+ </v-btn>
76
+ </form>
77
+
78
+ <!-- Register -->
79
+ <form v-else class="auth-form" @submit.prevent="submitRegister">
80
+ <v-text-field
81
+ v-model="registerForm.username"
82
+ label="Nombre de usuario"
83
+ variant="outlined"
84
+ density="comfortable"
85
+ prepend-inner-icon="mdi-account-outline"
86
+ autocomplete="username"
87
+ hide-details="auto"
88
+ class="mb-4"
89
+ />
90
+ <v-text-field
91
+ v-model="registerForm.password"
92
+ label="Contraseña"
93
+ :type="showRegPwd ? 'text' : 'password'"
94
+ variant="outlined"
95
+ density="comfortable"
96
+ prepend-inner-icon="mdi-lock-outline"
97
+ :append-inner-icon="showRegPwd ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
98
+ autocomplete="new-password"
99
+ hide-details="auto"
100
+ class="mb-4"
101
+ @click:append-inner="showRegPwd = !showRegPwd"
102
+ />
103
+ <v-alert v-if="registerError" type="error" variant="tonal" rounded="lg" density="compact" class="mb-4">
104
+ {{ registerError }}
105
+ </v-alert>
106
+ <v-btn type="submit" color="primary" variant="flat" block rounded="lg" size="large" :loading="registerLoading">
107
+ Crear cuenta
108
+ </v-btn>
109
+ </form>
110
+ </div>
111
+ </template>
112
+
113
+ <!-- ── Paso 2: Onboarding de películas vistas ─────────── -->
114
+ <template v-else>
115
+ <div class="auth-card-header">
116
+ <h2 class="card-title">¿Qué películas ya has visto?</h2>
117
+ <p class="card-subtitle">
118
+ Añade algunas y el recomendador aprenderá mejor tus gustos desde el primer momento.
119
+ </p>
120
+ </div>
121
+
122
+ <v-divider />
123
+
124
+ <div class="form-wrap onboarding-wrap">
125
+ <!-- Buscador -->
126
+ <div class="search-row">
127
+ <v-text-field
128
+ v-model="searchQuery"
129
+ label="Buscar película..."
130
+ variant="outlined"
131
+ density="comfortable"
132
+ prepend-inner-icon="mdi-magnify"
133
+ hide-details
134
+ clearable
135
+ @input="onSearchInput"
136
+ @click:clear="clearSearch"
137
+ />
138
+ </div>
139
+
140
+ <!-- Resultados de búsqueda -->
141
+ <div v-if="searchResults.length" class="results-list">
142
+ <div
143
+ v-for="peli in searchResults"
144
+ :key="peli.movie_id"
145
+ class="result-item"
146
+ :class="{ 'result-item--added': isAdded(peli.movie_id) }"
147
+ @click="togglePelicula(peli)"
148
+ >
149
+ <div class="result-info">
150
+ <span class="result-title">{{ peli.titulo }}</span>
151
+ <span class="result-genre">{{ formatGenre(peli.genero) }}</span>
152
+ </div>
153
+ <div class="result-right">
154
+ <span v-if="peli.rating_mean" class="result-rating">
155
+ <v-icon size="13" color="amber-darken-1">mdi-star</v-icon>
156
+ {{ peli.rating_mean.toFixed(1) }}
157
+ </span>
158
+ <v-icon v-if="isAdded(peli.movie_id)" color="primary" size="20">mdi-check-circle</v-icon>
159
+ <v-icon v-else size="20" color="grey-lighten-1">mdi-plus-circle-outline</v-icon>
160
+ </div>
161
+ </div>
162
+ </div>
163
+
164
+ <!-- Películas populares (cuando no hay búsqueda activa) -->
165
+ <div v-else-if="!searchQuery" class="popular-section">
166
+ <p class="section-label">Populares</p>
167
+ <div class="results-list">
168
+ <div
169
+ v-for="peli in popularMovies"
170
+ :key="peli.movie_id"
171
+ class="result-item"
172
+ :class="{ 'result-item--added': isAdded(peli.movie_id) }"
173
+ @click="togglePelicula(peli)"
174
+ >
175
+ <div class="result-info">
176
+ <span class="result-title">{{ peli.titulo }}</span>
177
+ <span class="result-genre">{{ formatGenre(peli.genero) }}</span>
178
+ </div>
179
+ <div class="result-right">
180
+ <span v-if="peli.rating_mean" class="result-rating">
181
+ <v-icon size="13" color="amber-darken-1">mdi-star</v-icon>
182
+ {{ peli.rating_mean.toFixed(1) }}
183
+ </span>
184
+ <v-icon v-if="isAdded(peli.movie_id)" color="primary" size="20">mdi-check-circle</v-icon>
185
+ <v-icon v-else size="20" color="grey-lighten-1">mdi-plus-circle-outline</v-icon>
186
+ </div>
187
+ </div>
188
+ </div>
189
+ </div>
190
+
191
+ <!-- Seleccionadas -->
192
+ <div v-if="seleccionadas.length" class="selected-section">
193
+ <p class="section-label">Añadidas ({{ seleccionadas.length }})</p>
194
+ <div class="chips-wrap">
195
+ <v-chip
196
+ v-for="peli in seleccionadas"
197
+ :key="peli.movie_id"
198
+ closable
199
+ size="small"
200
+ class="chip-pelicula"
201
+ @click:close="quitarPelicula(peli.movie_id)"
202
+ >
203
+ {{ peli.titulo }}
204
+ </v-chip>
205
+ </div>
206
+ </div>
207
+
208
+ <!-- Acciones -->
209
+ <div class="onboarding-actions">
210
+ <v-btn
211
+ color="primary"
212
+ variant="flat"
213
+ block
214
+ rounded="lg"
215
+ size="large"
216
+ :loading="onboardingLoading"
217
+ @click="finalizarOnboarding"
218
+ >
219
+ {{ seleccionadas.length ? `Guardar y empezar (${seleccionadas.length})` : 'Empezar sin historial' }}
220
+ </v-btn>
221
+ </div>
222
+ </div>
223
+ </template>
224
+
225
  </v-card>
226
  </div>
227
  </div>
 
229
  </template>
230
 
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");
237
+ const step = ref(1);
238
 
239
+ // ── Login ──────────────────────────────────────────────────────────
240
  const loginForm = ref({ username: "", password: "" });
241
  const loginError = ref("");
242
  const loginLoading = ref(false);
243
  const showLoginPwd = ref(false);
244
 
245
+ // ── Register ───────────────────────────────────────────────────────
246
+ const registerForm = ref({ username: "", password: "" });
247
  const registerError = ref("");
248
  const registerLoading = ref(false);
249
  const showRegPwd = ref(false);
250
 
251
+ // ── Onboarding ─────────────────────────────────────────────────────
252
+ const popularMovies = ref([]);
253
+ const searchQuery = ref("");
254
+ const searchResults = ref([]);
255
+ const seleccionadas = ref([]);
256
+ const onboardingLoading = ref(false);
257
+ let searchTimer = null;
258
+
259
+ // Credenciales guardadas tras el registro, para el onboarding
260
+ let _pendingUserId = "";
261
+ let _pendingToken = "";
262
+
263
  async function submitLogin() {
264
  loginError.value = "";
265
  if (!loginForm.value.username || !loginForm.value.password) {
 
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 }),
 
 
 
 
309
  });
310
  const data = await res.json();
311
  if (!res.ok) { registerError.value = data.error || "Error al crear la cuenta."; return; }
312
+ // Guardamos credenciales en localStorage ya, pero vamos al paso 2
313
  localStorage.setItem("vs_token", data.token);
314
  localStorage.setItem("vs_user_id", data.user_id);
315
  localStorage.setItem("vs_username", data.username);
316
+ _pendingUserId = data.user_id;
317
+ _pendingToken = data.token;
318
+ await cargarPopulares();
319
+ step.value = 2;
320
  } catch {
321
  registerError.value = "Error de conexión con el servidor.";
322
  } finally {
323
  registerLoading.value = false;
324
  }
325
  }
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 || [];
333
+ }
334
+ } catch {
335
+ // silencioso — no bloqueamos el onboarding si falla
336
+ }
337
+ }
338
+
339
+ function onSearchInput() {
340
+ clearTimeout(searchTimer);
341
+ if (!searchQuery.value || searchQuery.value.trim().length < 2) {
342
+ searchResults.value = [];
343
+ return;
344
+ }
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 || [];
352
+ }
353
+ } catch {
354
+ searchResults.value = [];
355
+ }
356
+ }, 300);
357
+ }
358
+
359
+ function clearSearch() {
360
+ searchQuery.value = "";
361
+ searchResults.value = [];
362
+ }
363
+
364
+ function isAdded(movieId) {
365
+ return seleccionadas.value.some(p => p.movie_id === movieId);
366
+ }
367
+
368
+ function togglePelicula(peli) {
369
+ if (isAdded(peli.movie_id)) {
370
+ quitarPelicula(peli.movie_id);
371
+ } else {
372
+ seleccionadas.value.push(peli);
373
+ }
374
+ }
375
+
376
+ function quitarPelicula(movieId) {
377
+ seleccionadas.value = seleccionadas.value.filter(p => p.movie_id !== movieId);
378
+ }
379
+
380
+ function formatGenre(genero) {
381
+ if (!genero || genero === "(no genres listed)") return "";
382
+ return genero.split("|").slice(0, 3).join(" · ");
383
+ }
384
+
385
+ async function finalizarOnboarding() {
386
+ if (!seleccionadas.value.length) {
387
+ router.push("/chat");
388
+ return;
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({
396
+ user_id: _pendingUserId,
397
+ token: _pendingToken,
398
+ peliculas: seleccionadas.value.map(p => ({
399
+ movie_id: p.movie_id,
400
+ titulo: p.titulo,
401
+ genero: p.genero,
402
+ })),
403
+ }),
404
+ });
405
+ } catch {
406
+ // Si falla el onboarding no bloqueamos al usuario
407
+ } finally {
408
+ onboardingLoading.value = false;
409
+ }
410
+ router.push("/chat");
411
+ }
412
  </script>
413
 
414
  <style scoped>
 
453
  position: relative;
454
  z-index: 1;
455
  width: 100%;
456
+ max-width: 480px;
457
  padding: 24px 16px;
458
  margin: 0 auto;
459
  display: flex;
 
544
 
545
  .auth-form { display: flex; flex-direction: column; }
546
 
547
+ /* ── Onboarding ─────────────────────────────────────────────────── */
548
+ .onboarding-wrap {
549
+ display: flex;
550
+ flex-direction: column;
551
+ gap: 16px;
552
+ }
553
+
554
+ .search-row { width: 100%; }
555
+
556
+ .results-list {
557
+ border: 1px solid var(--vs-border, rgba(102,126,234,0.15));
558
+ border-radius: 10px;
559
+ overflow: hidden;
560
+ max-height: 280px;
561
+ overflow-y: auto;
562
+ }
563
+
564
+ .result-item {
565
+ display: flex;
566
+ align-items: center;
567
+ justify-content: space-between;
568
+ padding: 10px 14px;
569
+ cursor: pointer;
570
+ transition: background 0.15s ease;
571
+ border-bottom: 1px solid rgba(102,126,234,0.07);
572
+ gap: 8px;
573
+ }
574
+
575
+ .result-item:last-child { border-bottom: none; }
576
+
577
+ .result-item:hover { background: rgba(102,126,234,0.06); }
578
+
579
+ .result-item--added { background: rgba(102,126,234,0.08); }
580
+
581
+ .result-info {
582
+ display: flex;
583
+ flex-direction: column;
584
+ gap: 2px;
585
+ min-width: 0;
586
+ }
587
+
588
+ .result-title {
589
+ font-size: 0.875rem;
590
+ font-weight: 500;
591
+ color: var(--vs-text, #1a1a2e);
592
+ white-space: nowrap;
593
+ overflow: hidden;
594
+ text-overflow: ellipsis;
595
+ max-width: 280px;
596
+ }
597
+
598
+ .result-genre {
599
+ font-size: 0.72rem;
600
+ color: var(--vs-muted, #6b7280);
601
+ white-space: nowrap;
602
+ overflow: hidden;
603
+ text-overflow: ellipsis;
604
+ max-width: 280px;
605
+ }
606
+
607
+ .result-right {
608
+ display: flex;
609
+ align-items: center;
610
+ gap: 6px;
611
+ flex-shrink: 0;
612
+ }
613
+
614
+ .result-rating {
615
+ display: flex;
616
+ align-items: center;
617
+ gap: 2px;
618
+ font-size: 0.78rem;
619
+ color: var(--vs-muted, #6b7280);
620
+ white-space: nowrap;
621
+ }
622
+
623
+ .section-label {
624
+ font-size: 0.72rem;
625
+ font-weight: 700;
626
+ text-transform: uppercase;
627
+ letter-spacing: 0.08em;
628
+ color: var(--vs-muted, #6b7280);
629
+ margin: 0 0 8px;
630
+ }
631
+
632
+ .popular-section, .selected-section { width: 100%; }
633
+
634
+ .chips-wrap {
635
+ display: flex;
636
+ flex-wrap: wrap;
637
+ gap: 6px;
638
+ }
639
+
640
+ .chip-pelicula {
641
+ font-size: 0.78rem;
642
+ }
643
+
644
+ .onboarding-actions { width: 100%; }
645
+
646
  @media (max-width: 480px) {
647
  .auth-card-header { padding: 20px 20px 12px; }
648
  .form-wrap { padding: 20px 20px 24px; }
649
+ .result-title, .result-genre { max-width: 180px; }
650
  }
651
  </style>
{notebooks/data/raw → data}/ml-latest-small/links.csv RENAMED
The diff for this file is too large to render. See raw diff
 
{notebooks/data/raw → data}/ml-latest-small/movies.csv RENAMED
The diff for this file is too large to render. See raw diff
 
{notebooks/data/raw → data}/ml-latest-small/ratings.csv RENAMED
The diff for this file is too large to render. See raw diff
 
{notebooks/data/raw → data}/ml-latest-small/tags.csv RENAMED
The diff for this file is too large to render. See raw diff
 
data/ml-latest/README.txt DELETED
@@ -1,179 +0,0 @@
1
- Summary
2
- =======
3
-
4
- This dataset (ml-latest) describes 5-star rating and free-text tagging activity from [MovieLens](http://movielens.org), a movie recommendation service. It contains 33832162 ratings and 2328315 tag applications across 86537 movies. These data were created by 330975 users between January 09, 1995 and July 20, 2023. This dataset was generated on July 20, 2023.
5
-
6
- Users were selected at random for inclusion. All selected users had rated at least 1 movies. No demographic information is included. Each user is represented by an id, and no other information is provided.
7
-
8
- The data are contained in the files `genome-scores.csv`, `genome-tags.csv`, `links.csv`, `movies.csv`, `ratings.csv` and `tags.csv`. More details about the contents and use of all these files follows.
9
-
10
- This is a *development* dataset. As such, it may change over time and is not an appropriate dataset for shared research results. See available *benchmark* datasets if that is your intent.
11
-
12
- This and other GroupLens data sets are publicly available for download at <http://grouplens.org/datasets/>.
13
-
14
-
15
- Usage License
16
- =============
17
-
18
- Neither the University of Minnesota nor any of the researchers involved can guarantee the correctness of the data, its suitability for any particular purpose, or the validity of results based on the use of the data set. The data set may be used for any research purposes under the following conditions:
19
-
20
- * The user may not state or imply any endorsement from the University of Minnesota or the GroupLens Research Group.
21
- * The user must acknowledge the use of the data set in publications resulting from the use of the data set (see below for citation information).
22
- * The user may redistribute the data set, including transformations, so long as it is distributed under these same license conditions.
23
- * The user may not use this information for any commercial or revenue-bearing purposes without first obtaining permission from a faculty member of the GroupLens Research Project at the University of Minnesota.
24
- * The executable software scripts are provided "as is" without warranty of any kind, either expressed or implied, including, but not limited to, the implied warranties of merchantability and fitness for a particular purpose. The entire risk as to the quality and performance of them is with you. Should the program prove defective, you assume the cost of all necessary servicing, repair or correction.
25
-
26
- In no event shall the University of Minnesota, its affiliates or employees be liable to you for any damages arising out of the use or inability to use these programs (including but not limited to loss of data or data being rendered inaccurate).
27
-
28
- If you have any further questions or comments, please email <grouplens-info@umn.edu>
29
-
30
-
31
- Citation
32
- ========
33
-
34
- To acknowledge use of the dataset in publications, please cite the following paper:
35
-
36
- > F. Maxwell Harper and Joseph A. Konstan. 2015. The MovieLens Datasets: History and Context. ACM Transactions on Interactive Intelligent Systems (TiiS) 5, 4: 19:1–19:19. <https://doi.org/10.1145/2827872>
37
-
38
-
39
- Further Information About GroupLens
40
- ===================================
41
-
42
- GroupLens is a research group in the Department of Computer Science and Engineering at the University of Minnesota. Since its inception in 1992, GroupLens's research projects have explored a variety of fields including:
43
-
44
- * recommender systems
45
- * online communities
46
- * mobile and ubiquitious technologies
47
- * digital libraries
48
- * local geographic information systems
49
-
50
- GroupLens Research operates a movie recommender based on collaborative filtering, MovieLens, which is the source of these data. We encourage you to visit <http://movielens.org> to try it out! If you have exciting ideas for experimental work to conduct on MovieLens, send us an email at <grouplens-info@cs.umn.edu> - we are always interested in working with external collaborators.
51
-
52
-
53
- Content and Use of Files
54
- ========================
55
-
56
- Formatting and Encoding
57
- -----------------------
58
-
59
- The dataset files are written as [comma-separated values](http://en.wikipedia.org/wiki/Comma-separated_values) files with a single header row. Columns that contain commas (`,`) are escaped using double-quotes (`"`). These files are encoded as UTF-8. If accented characters in movie titles or tag values (e.g. Misérables, Les (1995)) display incorrectly, make sure that any program reading the data, such as a text editor, terminal, or script, is configured for UTF-8.
60
-
61
-
62
- User Ids
63
- --------
64
-
65
- MovieLens users were selected at random for inclusion. Their ids have been anonymized. User ids are consistent between `ratings.csv` and `tags.csv` (i.e., the same id refers to the same user across the two files).
66
-
67
-
68
- Movie Ids
69
- ---------
70
-
71
- Only movies with at least one rating or tag are included in the dataset. These movie ids are consistent with those used on the MovieLens web site (e.g., id `1` corresponds to the URL <https://movielens.org/movies/1>). Movie ids are consistent between `ratings.csv`, `tags.csv`, `movies.csv`, and `links.csv` (i.e., the same id refers to the same movie across these four data files).
72
-
73
-
74
- Ratings Data File Structure (ratings.csv)
75
- -----------------------------------------
76
-
77
- All ratings are contained in the file `ratings.csv`. Each line of this file after the header row represents one rating of one movie by one user, and has the following format:
78
-
79
- userId,movieId,rating,timestamp
80
-
81
- The lines within this file are ordered first by userId, then, within user, by movieId.
82
-
83
- Ratings are made on a 5-star scale, with half-star increments (0.5 stars - 5.0 stars).
84
-
85
- Timestamps represent seconds since midnight Coordinated Universal Time (UTC) of January 1, 1970.
86
-
87
-
88
- Tags Data File Structure (tags.csv)
89
- -----------------------------------
90
-
91
- All tags are contained in the file `tags.csv`. Each line of this file after the header row represents one tag applied to one movie by one user, and has the following format:
92
-
93
- userId,movieId,tag,timestamp
94
-
95
- The lines within this file are ordered first by userId, then, within user, by movieId.
96
-
97
- Tags are user-generated metadata about movies. Each tag is typically a single word or short phrase. The meaning, value, and purpose of a particular tag is determined by each user.
98
-
99
- Timestamps represent seconds since midnight Coordinated Universal Time (UTC) of January 1, 1970.
100
-
101
-
102
- Movies Data File Structure (movies.csv)
103
- ---------------------------------------
104
-
105
- Movie information is contained in the file `movies.csv`. Each line of this file after the header row represents one movie, and has the following format:
106
-
107
- movieId,title,genres
108
-
109
- Movie titles are entered manually or imported from <https://www.themoviedb.org/>, and include the year of release in parentheses. Errors and inconsistencies may exist in these titles.
110
-
111
- Genres are a pipe-separated list, and are selected from the following:
112
-
113
- * Action
114
- * Adventure
115
- * Animation
116
- * Children's
117
- * Comedy
118
- * Crime
119
- * Documentary
120
- * Drama
121
- * Fantasy
122
- * Film-Noir
123
- * Horror
124
- * Musical
125
- * Mystery
126
- * Romance
127
- * Sci-Fi
128
- * Thriller
129
- * War
130
- * Western
131
- * (no genres listed)
132
-
133
-
134
- Links Data File Structure (links.csv)
135
- ---------------------------------------
136
-
137
- Identifiers that can be used to link to other sources of movie data are contained in the file `links.csv`. Each line of this file after the header row represents one movie, and has the following format:
138
-
139
- movieId,imdbId,tmdbId
140
-
141
- movieId is an identifier for movies used by <https://movielens.org>. E.g., the movie Toy Story has the link <https://movielens.org/movies/1>.
142
-
143
- imdbId is an identifier for movies used by <http://www.imdb.com>. E.g., the movie Toy Story has the link <http://www.imdb.com/title/tt0114709/>.
144
-
145
- tmdbId is an identifier for movies used by <https://www.themoviedb.org>. E.g., the movie Toy Story has the link <https://www.themoviedb.org/movie/862>.
146
-
147
- Use of the resources listed above is subject to the terms of each provider.
148
-
149
-
150
- Tag Genome (genome-scores.csv and genome-tags.csv)
151
- -------------------------------------------------
152
-
153
- This data set includes a current copy of the Tag Genome.
154
-
155
- [genome-paper]: http://files.grouplens.org/papers/tag_genome.pdf
156
-
157
- The tag genome is a data structure that contains tag relevance scores for movies. The structure is a dense matrix: each movie in the genome has a value for *every* tag in the genome.
158
-
159
- As described in [this article][genome-paper], the tag genome encodes how strongly movies exhibit particular properties represented by tags (atmospheric, thought-provoking, realistic, etc.). The tag genome was computed using a machine learning algorithm on user-contributed content including tags, ratings, and textual reviews.
160
-
161
- The genome is split into two files. The file `genome-scores.csv` contains movie-tag relevance data in the following format:
162
-
163
- movieId,tagId,relevance
164
-
165
- The second file, `genome-tags.csv`, provides the tag descriptions for the tag IDs in the genome file, in the following format:
166
-
167
- tagId,tag
168
-
169
- The `tagId` values are generated when the data set is exported, so they may vary from version to version of the MovieLens data sets.
170
-
171
- Please include the following citation if referencing tag genome data:
172
-
173
- > Jesse Vig, Shilad Sen, and John Riedl. 2012. The Tag Genome: Encoding Community Knowledge to Support Novel Interaction. ACM Trans. Interact. Intell. Syst. 2, 3: 13:1–13:44. <https://doi.org/10.1145/2362394.2362395>
174
-
175
-
176
- Cross-Validation
177
- ----------------
178
-
179
- Prior versions of the MovieLens dataset included either pre-computed cross-folds or scripts to perform this computation. We no longer bundle either of these features with the dataset, since most modern toolkits provide this as a built-in feature. If you wish to learn about standard approaches to cross-fold computation in the context of recommender systems evaluation, see [LensKit](http://lenskit.org) for tools, documentation, and open-source code examples.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
data/{download_movielens_large.py → movielens.py} RENAMED
@@ -7,7 +7,7 @@ from tempfile import NamedTemporaryFile
7
  from urllib.request import urlretrieve
8
  from zipfile import ZipFile
9
 
10
- MOVIELENS_LARGE_URL = "https://files.grouplens.org/datasets/movielens/ml-latest.zip"
11
 
12
 
13
  def download_zip(url: str, destination: Path) -> None:
@@ -41,16 +41,16 @@ def extract_csv_files(zip_path: Path, output_dir: Path) -> list[Path]:
41
 
42
  def main() -> None:
43
  parser = argparse.ArgumentParser(
44
- description="Descarga y extrae los CSV de MovieLens (version grande: ml-latest)."
45
  )
46
  parser.add_argument(
47
  "--url",
48
- default=MOVIELENS_LARGE_URL,
49
  help="URL del dataset ZIP de MovieLens.",
50
  )
51
  parser.add_argument(
52
  "--output",
53
- default=str(Path(__file__).resolve().parent / "ml-latest"),
54
  help="Directorio donde se guardaran los CSV extraidos.",
55
  )
56
  parser.add_argument(
 
7
  from urllib.request import urlretrieve
8
  from zipfile import ZipFile
9
 
10
+ MOVIELENS_SMALL_URL = "https://files.grouplens.org/datasets/movielens/ml-latest-small.zip"
11
 
12
 
13
  def download_zip(url: str, destination: Path) -> None:
 
41
 
42
  def main() -> None:
43
  parser = argparse.ArgumentParser(
44
+ description="Descarga y extrae los CSV de MovieLens (version pequeña: ml-latest-small)."
45
  )
46
  parser.add_argument(
47
  "--url",
48
+ default=MOVIELENS_SMALL_URL,
49
  help="URL del dataset ZIP de MovieLens.",
50
  )
51
  parser.add_argument(
52
  "--output",
53
+ default=str(Path(__file__).resolve().parent / "ml-latest-small"),
54
  help="Directorio donde se guardaran los CSV extraidos.",
55
  )
56
  parser.add_argument(
data/procesado/peliculas_100_emociones.csv DELETED
@@ -1,101 +0,0 @@
1
- movieId,title,genres,rating_count,rating_mean,estados_emocionales
2
- 296,Pulp Fiction (1994),Comedy|Crime|Drama|Thriller,108756,4.192,ira|alegria
3
- 593,"Silence of the Lambs, The (1991)",Crime|Horror|Thriller,101802,4.15,ira|sorpresa
4
- 260,Star Wars: Episode IV - A New Hope (1977),Action|Adventure|Sci-Fi,97202,4.092,alegria|ira
5
- 527,Schindler's List (1993),Drama|War,84232,4.242,tristeza|neutral
6
- 589,Terminator 2: Judgment Day (1991),Action|Sci-Fi,71820,3.962,ira|sorpresa
7
- 47,Seven (a.k.a. Se7en) (1995),Mystery|Thriller,65666,4.088,sorpresa|ira
8
- 6539,Pirates of the Caribbean: The Curse of the Black Pearl (2003),Action|Adventure|Comedy|Fantasy,50868,3.784,alegria|miedo
9
- 541,Blade Runner (1982),Action|Sci-Fi|Thriller,47695,4.114,ira|sorpresa
10
- 924,2001: A Space Odyssey (1968),Adventure|Drama|Sci-Fi,37113,3.997,alegria|tristeza
11
- 4896,Harry Potter and the Sorcerer's Stone (a.k.a. Harry Potter and the Philosopher's Stone) (2001),Adventure|Children|Fantasy,36127,3.695,alegria|miedo
12
- 919,"Wizard of Oz, The (1939)",Adventure|Children|Fantasy|Musical,30354,3.92,alegria|tristeza
13
- 8360,Shrek 2 (2004),Adventure|Animation|Children|Comedy|Musical|Romance,26972,3.478,alegria|tristeza
14
- 903,Vertigo (1958),Drama|Mystery|Romance|Thriller,20775,4.116,tristeza|sorpresa
15
- 4848,Mulholland Drive (2001),Crime|Drama|Film-Noir|Mystery|Thriller,17061,3.854,ira|sorpresa
16
- 673,Space Jam (1996),Adventure|Animation|Children|Comedy|Fantasy|Sci-Fi,14268,2.79,alegria|miedo
17
- 215,Before Sunrise (1995),Drama|Romance,8835,3.943,tristeza|neutral
18
- 4105,"Evil Dead, The (1981)",Fantasy|Horror|Thriller,7838,3.699,sorpresa|ira
19
- 3503,Solaris (Solyaris) (1972),Drama|Mystery|Sci-Fi,3705,3.883,sorpresa|tristeza
20
- 123,Chungking Express (Chung Hing sam lam) (1994),Drama|Mystery|Romance,3683,4.005,tristeza|sorpresa
21
- 275503,Aftersun (2022),Drama,403,3.999,tristeza|neutral
22
- 318,"Shawshank Redemption, The (1994)",Crime|Drama,122296,4.417,tristeza|ira
23
- 356,Forrest Gump (1994),Comedy|Drama|Romance|War,113581,4.068,tristeza|alegria
24
- 2571,"Matrix, The (1999)",Action|Sci-Fi|Thriller,107056,4.161,ira|sorpresa
25
- 2959,Fight Club (1999),Action|Crime|Drama|Thriller,86207,4.236,ira|neutral
26
- 480,Jurassic Park (1993),Action|Adventure|Sci-Fi|Thriller,83026,3.689,ira|sorpresa
27
- 1196,Star Wars: Episode V - The Empire Strikes Back (1980),Action|Adventure|Sci-Fi,80200,4.118,alegria|ira
28
- 4993,"Lord of the Rings: The Fellowship of the Ring, The (2001)",Adventure|Fantasy,79940,4.099,alegria|miedo
29
- 1,Toy Story (1995),Adventure|Animation|Children|Comedy|Fantasy,76813,3.894,alegria|miedo
30
- 1210,Star Wars: Episode VI - Return of the Jedi (1983),Action|Adventure|Sci-Fi,76773,3.981,alegria|ira
31
- 110,Braveheart (1995),Action|Drama|War,75514,3.996,neutral|tristeza
32
- 7153,"Lord of the Rings: The Return of the King, The (2003)",Action|Adventure|Drama|Fantasy,75512,4.11,neutral|alegria
33
- 1198,Raiders of the Lost Ark (Indiana Jones and the Raiders of the Lost Ark) (1981),Action|Adventure,75248,4.101,alegria|ira
34
- 858,"Godfather, The (1972)",Crime|Drama,75004,4.327,tristeza|ira
35
- 5952,"Lord of the Rings: The Two Towers, The (2002)",Adventure|Fantasy,73687,4.081,alegria|miedo
36
- 50,"Usual Suspects, The (1995)",Crime|Mystery|Thriller,72893,4.268,ira|sorpresa
37
- 2858,American Beauty (1999),Drama|Romance,69902,4.102,tristeza|neutral
38
- 1270,Back to the Future (1985),Adventure|Comedy|Sci-Fi,67777,3.958,alegria|miedo
39
- 58559,"Dark Knight, The (2008)",Action|Crime|Drama|IMAX,65349,4.188,ira|neutral
40
- 79132,Inception (2010),Action|Crime|Drama|Mystery|Sci-Fi|Thriller|IMAX,65056,4.176,ira|sorpresa
41
- 2028,Saving Private Ryan (1998),Action|Drama|War,64235,4.057,neutral|tristeza
42
- 780,Independence Day (a.k.a. ID4) (1996),Action|Adventure|Sci-Fi|Thriller,63687,3.402,ira|sorpresa
43
- 150,Apollo 13 (1995),Adventure|Drama|IMAX,62521,3.89,alegria|tristeza
44
- 608,Fargo (1996),Comedy|Crime|Drama|Thriller,61977,4.115,ira|alegria
45
- 457,"Fugitive, The (1993)",Thriller,61732,3.977,ira|sorpresa
46
- 3578,Gladiator (2000),Action|Adventure|Drama,60749,3.97,neutral|alegria
47
- 2762,"Sixth Sense, The (1999)",Drama|Horror|Mystery,60439,4.008,tristeza|sorpresa
48
- 32,Twelve Monkeys (a.k.a. 12 Monkeys) (1995),Mystery|Sci-Fi|Thriller,59730,3.897,sorpresa|ira
49
- 4306,Shrek (2001),Adventure|Animation|Children|Comedy|Fantasy|Romance,58529,3.749,alegria|miedo
50
- 592,Batman (1989),Action|Crime|Thriller,56330,3.39,ira|sorpresa
51
- 588,Aladdin (1992),Adventure|Animation|Children|Comedy|Musical,55791,3.702,alegria|miedo
52
- 4226,Memento (2000),Mystery|Thriller,55649,4.144,sorpresa|ira
53
- 1704,Good Will Hunting (1997),Drama|Romance,54980,4.107,tristeza|neutral
54
- 364,"Lion King, The (1994)",Adventure|Animation|Children|Drama|Musical|IMAX,53509,3.833,alegria|tristeza
55
- 590,Dances with Wolves (1990),Adventure|Drama|Western,53377,3.741,alegria|tristeza
56
- 380,True Lies (1994),Action|Adventure|Comedy|Romance|Thriller,52789,3.505,alegria|ira
57
- 1197,"Princess Bride, The (1987)",Action|Adventure|Comedy|Fantasy|Romance,50775,4.111,alegria|miedo
58
- 1721,Titanic (1997),Drama|Romance,50706,3.429,tristeza|neutral
59
- 1580,Men in Black (a.k.a. MIB) (1997),Action|Comedy|Sci-Fi,49951,3.595,alegria|ira
60
- 1193,One Flew Over the Cuckoo's Nest (1975),Drama,49316,4.213,tristeza|neutral
61
- 377,Speed (1994),Action|Romance|Thriller,49029,3.491,ira|tristeza
62
- 1291,Indiana Jones and the Last Crusade (1989),Action|Adventure,48979,3.982,alegria|ira
63
- 1240,"Terminator, The (1984)",Action|Sci-Fi|Thriller,48672,3.902,ira|sorpresa
64
- 4886,"Monsters, Inc. (2001)",Adventure|Animation|Children|Comedy|Fantasy,48441,3.841,alegria|miedo
65
- 6377,Finding Nemo (2003),Adventure|Animation|Children|Comedy,48124,3.821,alegria|miedo
66
- 1265,Groundhog Day (1993),Comedy|Fantasy|Romance,47956,3.904,miedo|alegria
67
- 1136,Monty Python and the Holy Grail (1975),Adventure|Comedy|Fantasy,47845,4.137,alegria|miedo
68
- 344,Ace Ventura: Pet Detective (1994),Comedy,47829,3.001,alegria|miedo
69
- 648,Mission: Impossible (1996),Action|Adventure|Mystery|Thriller,47759,3.414,ira|sorpresa
70
- 1036,Die Hard (1988),Action|Crime|Thriller,47472,3.943,ira|sorpresa
71
- 1221,"Godfather: Part II, The (1974)",Crime|Drama,47271,4.27,tristeza|ira
72
- 6874,Kill Bill: Vol. 1 (2003),Action|Crime|Thriller,46973,3.856,ira|sorpresa
73
- 1214,Alien (1979),Horror|Sci-Fi,46572,4.07,sorpresa
74
- 7361,Eternal Sunshine of the Spotless Mind (2004),Drama|Romance|Sci-Fi,46292,4.07,tristeza|sorpresa
75
- 1682,"Truman Show, The (1998)",Comedy|Drama|Sci-Fi,45809,3.892,alegria|tristeza
76
- 4973,"Amelie (Fabuleux destin d'Amélie Poulain, Le) (2001)",Comedy|Romance,45749,4.087,alegria|tristeza
77
- 595,Beauty and the Beast (1991),Animation|Children|Fantasy|Musical|Romance|IMAX,45404,3.68,alegria|tristeza
78
- 1089,Reservoir Dogs (1992),Crime|Mystery|Thriller,45318,4.094,ira|sorpresa
79
- 1213,Goodfellas (1990),Crime|Drama,44592,4.192,tristeza|ira
80
- 1097,E.T. the Extra-Terrestrial (1982),Children|Drama|Sci-Fi,43868,3.738,alegria|tristeza
81
- 293,Léon: The Professional (a.k.a. The Professional) (Léon) (1994),Action|Crime|Drama|Thriller,43539,4.098,ira|neutral
82
- 165,Die Hard: With a Vengeance (1995),Action|Crime|Thriller,43336,3.516,ira|sorpresa
83
- 33794,Batman Begins (2005),Action|Crime|IMAX,43300,3.925,ira|neutral
84
- 4995,"Beautiful Mind, A (2001)",Drama|Romance,43013,3.966,tristeza|neutral
85
- 8961,"Incredibles, The (2004)",Action|Adventure|Animation|Children|Comedy,42953,3.85,alegria|miedo
86
- 3793,X-Men (2000),Action|Adventure|Sci-Fi,42387,3.534,alegria|ira
87
- 4963,Ocean's Eleven (2001),Crime|Thriller,42340,3.817,ira|sorpresa
88
- 1527,"Fifth Element, The (1997)",Action|Adventure|Comedy|Sci-Fi,42155,3.784,alegria|ira
89
- 60069,WALL·E (2008),Adventure|Animation|Children|Romance|Sci-Fi,42033,4.014,alegria|tristeza
90
- 367,"Mask, The (1994)",Action|Comedy|Crime|Fantasy,41407,3.19,ira|miedo
91
- 3147,"Green Mile, The (1999)",Crime|Drama,41222,4.043,tristeza|ira
92
- 2628,Star Wars: Episode I - The Phantom Menace (1999),Action|Adventure|Sci-Fi,41061,3.078,alegria|ira
93
- 2329,American History X (1998),Crime|Drama,41055,4.135,tristeza|ira
94
- 500,Mrs. Doubtfire (1993),Comedy|Drama,40844,3.398,alegria|tristeza
95
- 597,Pretty Woman (1990),Comedy|Romance,40755,3.439,alegria|tristeza
96
- 109487,Interstellar (2014),Sci-Fi|IMAX,40603,4.147,sorpresa
97
- 5445,Minority Report (2002),Action|Crime|Mystery|Sci-Fi|Thriller,40414,3.696,ira|sorpresa
98
- 733,"Rock, The (1996)",Action|Adventure|Thriller,40412,3.687,ira|alegria
99
- 231,Dumb & Dumber (Dumb and Dumber) (1994),Adventure|Comedy,40371,2.976,alegria|miedo
100
- 1258,"Shining, The (1980)",Horror,40297,4.036,neutral
101
- 1200,Aliens (1986),Action|Adventure|Horror|Sci-Fi,40182,4.006,alegria|ira
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
data/procesado/peliculas_conocidas.csv DELETED
@@ -1,21 +0,0 @@
1
- movieId,title,genres,estados_emocionales
2
- 47,Seven (a.k.a. Se7en) (1995),Mystery|Thriller,miedo|sorpresa
3
- 123,Chungking Express (Chung Hing sam lam) (1994),Drama|Mystery|Romance,tristeza|neutral
4
- 215,Before Sunrise (1995),Drama|Romance,alegria|neutral
5
- 260,Star Wars: Episode IV - A New Hope (1977),Action|Adventure|Sci-Fi,alegria|sorpresa
6
- 296,Pulp Fiction (1994),Comedy|Crime|Drama|Thriller,sorpresa|neutral
7
- 527,Schindler's List (1993),Drama|War,tristeza|neutral
8
- 541,Blade Runner (1982),Action|Sci-Fi|Thriller,neutral|sorpresa
9
- 589,Terminator 2: Judgment Day (1991),Action|Sci-Fi,sorpresa|alegria
10
- 593,"Silence of the Lambs, The (1991)",Crime|Horror|Thriller,miedo|asco
11
- 673,Space Jam (1996),Adventure|Animation|Children|Comedy|Fantasy|Sci-Fi,alegria|neutral
12
- 903,Vertigo (1958),Drama|Mystery|Romance|Thriller,miedo|sorpresa
13
- 919,"Wizard of Oz, The (1939)",Adventure|Children|Fantasy|Musical,alegria|sorpresa
14
- 924,2001: A Space Odyssey (1968),Adventure|Drama|Sci-Fi,neutral|sorpresa
15
- 4105,"Evil Dead, The (1981)",Fantasy|Horror|Thriller,miedo|asco
16
- 4848,Mulholland Drive (2001),Crime|Drama|Film-Noir|Mystery|Thriller,sorpresa|neutral
17
- 4896,Harry Potter and the Sorcerer's Stone (a.k.a. Harry Potter and the Philosopher's Stone) (2001),Adventure|Children|Fantasy,alegria|sorpresa
18
- 3503,Solaris (Solyaris) (1972),Drama|Mystery|Sci-Fi,tristeza|neutral
19
- 8360,Shrek 2 (2004),Adventure|Animation|Children|Comedy|Musical|Romance,alegria|neutral
20
- 275503,Aftersun (2022),Drama,tristeza|neutral
21
- 6539,Pirates of the Caribbean: The Curse of the Black Pearl (2003),Action|Adventure|Comedy|Fantasy,alegria|sorpresa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
data/script.py DELETED
@@ -1,174 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import csv
4
- from pathlib import Path
5
- from urllib.request import urlretrieve
6
- from zipfile import ZipFile
7
-
8
-
9
- MOVIELENS_LARGE_URL = "https://files.grouplens.org/datasets/movielens/ml-latest.zip"
10
- TARGET_SIZE = 100
11
-
12
- # Esquema base de asignacion genero -> emocion (Ekman + neutral).
13
- EMOTION_GENRE_MAP = {
14
- "alegria": {"Comedy", "Animation", "Children", "Adventure", "Musical"},
15
- "tristeza": {"Drama", "Romance", "Musical"},
16
- "ira": {"Thriller", "Action", "Crime"},
17
- "miedo": {"Fantasy", "Animation", "Comedy"},
18
- "asco": {"Documentary", "Comedy"},
19
- "sorpresa": {"Mystery", "Sci-Fi", "Fantasy", "Thriller"},
20
- "neutral": {"Drama", "Documentary", "Action"},
21
- }
22
-
23
- # En empates, priorizamos emociones especificas sobre neutral.
24
- EMOTION_PRIORITY = ["alegria", "tristeza", "ira", "miedo", "asco", "sorpresa", "neutral"]
25
-
26
-
27
- def download_movielens_large(base_dir: Path) -> Path:
28
- zip_path = base_dir / "ml-latest.zip"
29
- extracted_dir = base_dir / "ml-latest"
30
-
31
- if extracted_dir.exists():
32
- print(f"El dataset ya existe en: {extracted_dir}")
33
- return extracted_dir
34
-
35
- print("Descargando MovieLens (ml-latest)...")
36
- urlretrieve(MOVIELENS_LARGE_URL, zip_path)
37
-
38
- print(f"Extrayendo en: {base_dir}")
39
- with ZipFile(zip_path, "r") as zip_file:
40
- zip_file.extractall(base_dir)
41
-
42
- zip_path.unlink(missing_ok=True)
43
- print(f"Descarga completa. Dataset disponible en: {extracted_dir}")
44
- return extracted_dir
45
-
46
-
47
- def infer_emotions_from_genres(genres: str) -> str:
48
- tokens = set((genres or "").split("|"))
49
- scores = {emotion: 0 for emotion in EMOTION_GENRE_MAP}
50
-
51
- for emotion, mapped_genres in EMOTION_GENRE_MAP.items():
52
- scores[emotion] = len(tokens.intersection(mapped_genres))
53
-
54
- priority_idx = {emotion: idx for idx, emotion in enumerate(EMOTION_PRIORITY)}
55
- ordered = sorted(scores.items(), key=lambda x: (-x[1], priority_idx.get(x[0], 999)))
56
- selected = [name for name, score in ordered if score > 0][:2]
57
- if not selected:
58
- return "neutral"
59
- return "|".join(selected)
60
-
61
-
62
- def build_emotion_dataset(base_dir: Path, target_size: int = TARGET_SIZE) -> Path:
63
- movielens_dir = base_dir / "ml-latest"
64
- movies_path = movielens_dir / "movies.csv"
65
- ratings_path = movielens_dir / "ratings.csv"
66
- seed_path = base_dir / "procesado" / "peliculas_conocidas.csv"
67
-
68
- if not movies_path.exists() or not ratings_path.exists():
69
- raise FileNotFoundError("No se encontraron movies.csv o ratings.csv en data/ml-latest")
70
-
71
- movies_by_id: dict[int, dict[str, str]] = {}
72
- with open(movies_path, "r", encoding="utf-8", newline="") as f:
73
- reader = csv.DictReader(f)
74
- for row in reader:
75
- movie_id = int(row["movieId"])
76
- movies_by_id[movie_id] = {
77
- "movieId": str(movie_id),
78
- "title": row.get("title", ""),
79
- "genres": row.get("genres", ""),
80
- }
81
-
82
- rating_acc: dict[int, tuple[int, float]] = {}
83
- with open(ratings_path, "r", encoding="utf-8", newline="") as f:
84
- reader = csv.DictReader(f)
85
- for row in reader:
86
- movie_id = int(row["movieId"])
87
- rating_val = float(row["rating"])
88
- count, total = rating_acc.get(movie_id, (0, 0.0))
89
- rating_acc[movie_id] = (count + 1, total + rating_val)
90
-
91
- ranked: list[dict[str, str | int | float]] = []
92
- for movie_id, movie in movies_by_id.items():
93
- count, total = rating_acc.get(movie_id, (0, 0.0))
94
- mean = (total / count) if count else 0.0
95
- ranked.append(
96
- {
97
- "movieId": movie_id,
98
- "title": movie["title"],
99
- "genres": movie["genres"],
100
- "rating_count": count,
101
- "rating_mean": round(mean, 3),
102
- }
103
- )
104
-
105
- ranked.sort(key=lambda x: (x["rating_count"], x["rating_mean"]), reverse=True)
106
-
107
- seed_ids: list[int] = []
108
- if seed_path.exists():
109
- with open(seed_path, "r", encoding="utf-8", newline="") as f:
110
- reader = csv.DictReader(f)
111
- for row in reader:
112
- raw_id = row.get("movieId", "").strip()
113
- if raw_id.isdigit():
114
- seed_ids.append(int(raw_id))
115
-
116
- seed_set = set(seed_ids)
117
- seed_rows = [r for r in ranked if r["movieId"] in seed_set]
118
- remaining_rows = [r for r in ranked if r["movieId"] not in seed_set]
119
-
120
- selected: list[dict[str, str | int | float]] = []
121
- seen: set[int] = set()
122
- for row in seed_rows + remaining_rows:
123
- movie_id = int(row["movieId"])
124
- if movie_id in seen:
125
- continue
126
- seen.add(movie_id)
127
- selected.append(row)
128
- if len(selected) >= target_size:
129
- break
130
-
131
- for row in selected:
132
- row["estados_emocionales"] = infer_emotions_from_genres(str(row["genres"]))
133
-
134
- out_path = base_dir / "procesado" / "peliculas_100_emociones.csv"
135
- out_path.parent.mkdir(parents=True, exist_ok=True)
136
-
137
- with open(out_path, "w", encoding="utf-8", newline="") as f:
138
- writer = csv.DictWriter(
139
- f,
140
- fieldnames=[
141
- "movieId",
142
- "title",
143
- "genres",
144
- "rating_count",
145
- "rating_mean",
146
- "estados_emocionales",
147
- ],
148
- )
149
- writer.writeheader()
150
- for row in selected:
151
- writer.writerow(
152
- {
153
- "movieId": row["movieId"],
154
- "title": row["title"],
155
- "genres": row["genres"],
156
- "rating_count": row["rating_count"],
157
- "rating_mean": row["rating_mean"],
158
- "estados_emocionales": row["estados_emocionales"],
159
- }
160
- )
161
-
162
- print(f"Dataset generado en: {out_path}")
163
- print(f"Peliculas guardadas: {len(selected)}")
164
- return out_path
165
-
166
-
167
- def main() -> None:
168
- base_dir = Path(__file__).resolve().parent
169
- download_movielens_large(base_dir)
170
- build_emotion_dataset(base_dir, target_size=TARGET_SIZE)
171
-
172
-
173
- if __name__ == "__main__":
174
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
data/textos_complejo.csv DELETED
@@ -1,43 +0,0 @@
1
- id,texto,esperado
2
- 1,"¡Me acaban de dar la beca! Estoy eufórico, es el mejor día de mi vida.",joy
3
- 2,"Hoy he aprobado todas las asignaturas con matrícula de honor. ¡Soy el más feliz!",joy
4
- 3,"Mi hija dio sus primeros pasos esta mañana. Me llené de alegría y orgullo.",joy
5
- 4,"¡Gané el concurso! No me lo puedo creer, estoy llorando de la emoción y la felicidad.",joy
6
- 5,"Después de meses de esfuerzo, por fin terminé mi tesis. ¡Estoy contentísimo!",joy
7
- 6,"Reencontrarme con mis amigos de la infancia después de diez años fue una maravilla.",joy
8
- 7,"No puedo parar de llorar. Echo mucho de menos a mi abuela desde que falleció.",sadness
9
- 8,"Perdí el trabajo hoy. No sé cómo voy a pagar el alquiler este mes.",sadness
10
- 9,"Se fue y no dijo adiós. Me dejó un vacío que no sé cómo llenar.",sadness
11
- 10,"Mi perro murió esta noche. Era mi compañero de los últimos doce años y le echo de menos.",sadness
12
- 11,"Me siento muy solo últimamente. Nadie parece entender por lo que estoy pasando.",sadness
13
- 12,"Suspendí el examen final tras meses estudiando. Siento que no sirvo para nada.",sadness
14
- 13,"¡Cómo se atreven a tratarme así! Esto es completamente inaceptable y lo pagarán.",anger
15
- 14,"Me robaron la mochila con el ordenador dentro. Estoy temblando de rabia.",anger
16
- 15,"Llevan tres horas de retraso y ni siquiera se molestan en dar explicaciones. ¡Indignante!",anger
17
- 16,"Me mintieron deliberadamente para quedarse con mi dinero. Estoy furioso y no lo voy a dejar pasar.",anger
18
- 17,"¡No puedo más! Han vuelto a ignorar mis correos después de semanas esperando respuesta.",anger
19
- 18,"Le dije claramente que no tocara mis cosas y lo hizo igualmente. Me saca de quicio.",anger
20
- 19,"Escuché pasos en el pasillo oscuro y estaba completamente solo en casa.",fear
21
- 20,"El médico dijo que necesitaba más pruebas. No paro de pensar en lo peor y no puedo dormir.",fear
22
- 21,"El avión empezó a moverse de forma extraña y sentí que el corazón se me paraba.",fear
23
- 22,"Me persiguió un perro enorme ladrando durante dos manzanas. Seguía temblando cuando llegué a casa.",fear
24
- 23,"Mañana es la operación y no sé si saldré bien. Tengo un miedo horrible.",fear
25
- 24,"El terremoto me pilló despierto. Nunca he sentido tanto pánico en toda mi vida.",fear
26
- 25,"¿Viniste desde tan lejos solo para verme? ¡No me lo esperaba para nada!",surprise
27
- 26,"Abrí la puerta y estaba toda mi familia reunida para darme una fiesta sorpresa. ¡Alucinante!",surprise
28
- 27,"No puedo creer que hayan cancelado ese programa. Llevaba diez años en antena.",surprise
29
- 28,"Me quedé boquiabierto cuando me dijeron que habían encontrado petróleo en el patio de mi casa.",surprise
30
- 29,"¡Apareció el gato que dábamos por perdido hace dos años! Estamos todos en shock.",surprise
31
- 30,"De repente me llamaron para decirme que me habían seleccionado sin haber mandado el currículum.",surprise
32
- 31,"El olor de esa basura podrida era tan nauseabundo que casi vomito.",disgust
33
- 32,"Encontré un gusano en la ensalada a mitad de comer. Me dan náuseas solo de recordarlo.",disgust
34
- 33,"El baño estaba tan sucio y lleno de bichos que no pude entrar. Repugnante.",disgust
35
- 34,"Ese comentario racista me revolvió el estómago. ¿Cómo puede pensar así alguien en pleno siglo XXI?",disgust
36
- 35,"Vi cómo maltrataban al animal y sentí una asco profundo hacia esa persona.",disgust
37
- 36,"Me sirvieron la carne completamente cruda. Asqueroso. Me negué a comer.",disgust
38
- 37,"La reunión está programada para las 15:00 en la sala de conferencias principal.",neutral
39
- 38,"El informe trimestral consta de cuarenta páginas y debe entregarse antes del viernes.",neutral
40
- 39,"La tienda abre de lunes a sábado de 9 a 21 horas. Los domingos permanece cerrada.",neutral
41
- 40,"El tren de las 8:42 hace parada en las estaciones de Atocha, Chamartín y Alcalá.",neutral
42
- 41,"El departamento de recursos humanos enviará el contrato por correo electrónico.",neutral
43
- 42,"La temperatura máxima prevista para mañana es de 18 grados centígrados.",neutral
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
data/textos_simple.csv DELETED
@@ -1,43 +0,0 @@
1
- id,texto,esperado
2
- 1,"Me encanta esta película, es increíble y me ha hecho sentir muy feliz. ¡Totalmente recomendada!",Positivo
3
- 2,"Qué decepción tan grande. El guión es pésimo, los actores no convencen y el final es un desastre.",Negativo
4
- 3,"La película dura dos horas y fue estrenada en octubre. Está disponible en la plataforma de streaming.",Neutro
5
- 4,"Los efectos visuales son espectaculares y la fotografía es preciosa, pero el ritmo es muy lento y aburre bastante.",Negativo
6
- 5,"Claro, porque esperar tres semanas para que llegue el DVD y que venga rayado es exactamente lo que esperaba de un servicio premium.",Negativo
7
- 6,"Una obra maestra del cine contemporáneo. Cada escena está cuidada al detalle y las actuaciones son sublimes.",Positivo
8
- 7,"¡La mejor serie que he visto en años! No podía dejar de verla, me enganchó desde el primer capítulo.",Positivo
9
- 8,"Entretenida y con buenos momentos de humor. Perfecta para pasar una tarde sin complicaciones.",Positivo
10
- 9,"El director ha conseguido crear una atmósfera única. La banda sonora es espectacular.",Positivo
11
- 10,"Simplemente perfecta. Una experiencia cinematográfica que no olvidaré jamás.",Positivo
12
- 11,"Un completo desastre. Dos horas de mi vida que nunca recuperaré.",Negativo
13
- 12,"Es la peor película que he visto en mucho tiempo. Aburrida, predecible y mal actuada.",Negativo
14
- 13,"No me gustó nada. El director claramente no sabía lo que estaba haciendo.",Negativo
15
- 14,"Malísima. Una pérdida total de tiempo y dinero. No la recomiendo para nada.",Negativo
16
- 15,"Terrible en todos los aspectos. Desde el guión hasta las actuaciones, todo está mal.",Negativo
17
- 16,"Decepcionante. Esperaba mucho más de este director.",Negativo
18
- 17,"Una película sin más. Cumple pero no destaca en nada particular.",Neutro
19
- 18,"Es correcta, aunque no aporta nada nuevo al género. Entretenimiento básico.",Neutro
20
- 19,"Está bien para pasar el rato, pero no es memorable.",Neutro
21
- 20,"Una producción estándar que cumple con las expectativas mínimas.",Neutro
22
- 21,"Tiene momentos brillantes pero también partes muy flojas. Un poco irregular.",Neutro
23
- 22,"Los actores principales están geniales, pero la trama se complica demasiado hacia el final.",Positivo
24
- 23,"Excelente fotografía y banda sonora, aunque el ritmo es inconsistente a veces.",Positivo
25
- 24,"Me gustó el primer acto, pero se vuelve confusa en la segunda mitad.",Neutro
26
- 25,"Buena idea inicial que se ejecuta de manera desigual. Algunos aciertos y algunos errores.",Neutro
27
- 26,"Las actuaciones son sólidas pero el guión tiene algunos agujeros importantes.",Positivo
28
- 27,"Interesante propuesta visual, aunque la historia no termina de convencer del todo.",Negativo
29
- 28,"Disfrutable en general, con altibajos que la hacen irregular pero entretenida.",Positivo
30
- 29,"Sí, claro, porque una película de tres horas sin argumento es exactamente lo que necesitaba ver hoy.",Negativo
31
- 30,"Por supuesto, gastar 15 euros en el cine para ver esto ha sido una inversión excelente.",Negativo
32
- 31,"Fantástico, otra secuela innecesaria que nadie pidió. Justo lo que el mundo necesitaba.",Negativo
33
- 32,"Magnífico trabajo del director, especialmente en la parte donde no pasa absolutamente nada interesante.",Negativo
34
- 33,"Maravilloso, porque esperar 30 minutos para que aparezca el primer diálogo es súper emocionante.",Negativo
35
- 34,"Genial, otra película que trata de ser profunda pero solo consigue ser aburrida.",Negativo
36
- 35,"Me encanta esta serie 😍 Es perfecta! 🎬✨",Positivo
37
- 36,"Qué horror de película 😤 No la recomiendo para nada 👎",Negativo
38
- 37,"Una experiencia cinematográfica increíble 🚀 ¡Totalmente recomendada! ⭐⭐⭐⭐⭐",Positivo
39
- 38,"Aburrida y predecible. Nada que destacar.",Negativo
40
- 39,"El final me dejó completamente sorprendido. Excelente giro argumental.",Positivo
41
- 40,"Los personajes están muy bien desarrollados. Se nota el trabajo del guionista.",Positivo
42
- 41,"La cinematografía es impresionante, cada plano es una obra de arte.",Positivo
43
- 42,"No logra mantener el interés. Se hace larga y tediosa.",Negativo
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
docs/diagrama_er.md CHANGED
@@ -1,13 +1,8 @@
1
- # Diagrama Entidad-Relación — ValorSentimental
2
-
3
- Esquema normalizado en **BCNF (Boyce-Codd Normal Form)**.
4
-
5
  ```mermaid
6
  erDiagram
7
  Usuarios {
8
  TEXT id PK
9
  TEXT username UK "NOT NULL"
10
- TEXT email
11
  TEXT password_hash "NOT NULL"
12
  TEXT session_token
13
  TEXT created_at "NOT NULL"
@@ -16,9 +11,7 @@ erDiagram
16
  Peliculas {
17
  TEXT id PK "IMDb/OMDB id"
18
  TEXT titulo "NOT NULL"
19
- TEXT anio
20
  TEXT genero
21
- TEXT poster_url
22
  }
23
 
24
  Emociones {
@@ -44,20 +37,21 @@ erDiagram
44
  INTEGER id PK
45
  TEXT user_id FK
46
  INTEGER emocion_pre_id FK "NOT NULL"
 
 
47
  INTEGER estrategia "NOT NULL"
48
  TEXT creado_en "NOT NULL"
49
- TEXT pelicula_id FK "nullable"
50
- INTEGER emocion_post_id FK "nullable"
51
  }
52
 
53
- Usuarios ||--o{ Emociones : "registra"
54
- Usuarios ||--o{ Historial_Peliculas : "visualiza"
55
- Usuarios ||--o{ Ciclo_Recomendacion : "inicia"
 
56
  Peliculas ||--o{ Historial_Peliculas : "aparece en"
57
  Peliculas ||--o{ Ciclo_Recomendacion : "recomendada en"
58
- Emociones ||--o{ Historial_Peliculas : "emocion_id (al ver)"
59
- Emociones ||--o{ Ciclo_Recomendacion : "emocion_pre_id"
60
- Emociones ||--o{ Ciclo_Recomendacion : "emocion_post_id"
61
  ```
62
 
63
  ## Justificación BCNF
 
 
 
 
 
1
  ```mermaid
2
  erDiagram
3
  Usuarios {
4
  TEXT id PK
5
  TEXT username UK "NOT NULL"
 
6
  TEXT password_hash "NOT NULL"
7
  TEXT session_token
8
  TEXT created_at "NOT NULL"
 
11
  Peliculas {
12
  TEXT id PK "IMDb/OMDB id"
13
  TEXT titulo "NOT NULL"
 
14
  TEXT genero
 
15
  }
16
 
17
  Emociones {
 
37
  INTEGER id PK
38
  TEXT user_id FK
39
  INTEGER emocion_pre_id FK "NOT NULL"
40
+ INTEGER emocion_post_id FK "nullable"
41
+ TEXT pelicula_id FK "nullable"
42
  INTEGER estrategia "NOT NULL"
43
  TEXT creado_en "NOT NULL"
 
 
44
  }
45
 
46
+ Usuarios ||--o{ Emociones : "registra"
47
+ Usuarios ||--o{ Historial_Peliculas : "visualiza"
48
+ Usuarios ||--o{ Ciclo_Recomendacion : "inicia"
49
+
50
  Peliculas ||--o{ Historial_Peliculas : "aparece en"
51
  Peliculas ||--o{ Ciclo_Recomendacion : "recomendada en"
52
+
53
+ Emociones ||--o{ Historial_Peliculas : "asociada a visionado"
54
+ Emociones ||--o{ Ciclo_Recomendacion : "estado inicial/final"
55
  ```
56
 
57
  ## Justificación BCNF
docs/login_seq.md ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ```mermaid
2
+ sequenceDiagram
3
+ participant Cliente as Cliente API/Vue
4
+ participant Servicio as Servicio
5
+ participant DAO as UsuarioDao
6
+ participant Singleton as ConexionBD
7
+ participant BD as SQLite
8
+
9
+ Cliente->>Servicio: login(username, password)
10
+ Servicio->>DAO: login(username, password)
11
+ DAO->>Singleton: instancia()
12
+ Singleton-->>DAO: ConexionBD unica
13
+ DAO->>Singleton: obtener_conexion()
14
+ Singleton-->>DAO: sqlite3.Connection
15
+ DAO->>BD: SELECT WHERE username
16
+ BD-->>DAO: Row
17
+ DAO->>DAO: check_password_hash()
18
+ alt Contrasena OK
19
+ DAO->>DAO: generar token UUID
20
+ DAO->>BD: UPDATE session_token
21
+ BD-->>DAO: OK
22
+ DAO-->>Servicio: Usuario(id, username, token)
23
+ Servicio-->>Cliente: user Usuario
24
+ else Contrasena incorrecta
25
+ DAO-->>Servicio: None
26
+ Servicio-->>Cliente: error Credenciales invalidas
27
+ end
28
+ ```
notebooks/01_exploracion_apis.ipynb DELETED
The diff for this file is too large to render. See raw diff
 
notebooks/02_exploracion_datasets.ipynb DELETED
The diff for this file is too large to render. See raw diff
 
notebooks/data/raw/ml-latest-small/README.txt DELETED
@@ -1,153 +0,0 @@
1
- Summary
2
- =======
3
-
4
- This dataset (ml-latest-small) describes 5-star rating and free-text tagging activity from [MovieLens](http://movielens.org), a movie recommendation service. It contains 100836 ratings and 3683 tag applications across 9742 movies. These data were created by 610 users between March 29, 1996 and September 24, 2018. This dataset was generated on September 26, 2018.
5
-
6
- Users were selected at random for inclusion. All selected users had rated at least 20 movies. No demographic information is included. Each user is represented by an id, and no other information is provided.
7
-
8
- The data are contained in the files `links.csv`, `movies.csv`, `ratings.csv` and `tags.csv`. More details about the contents and use of all these files follows.
9
-
10
- This is a *development* dataset. As such, it may change over time and is not an appropriate dataset for shared research results. See available *benchmark* datasets if that is your intent.
11
-
12
- This and other GroupLens data sets are publicly available for download at <http://grouplens.org/datasets/>.
13
-
14
-
15
- Usage License
16
- =============
17
-
18
- Neither the University of Minnesota nor any of the researchers involved can guarantee the correctness of the data, its suitability for any particular purpose, or the validity of results based on the use of the data set. The data set may be used for any research purposes under the following conditions:
19
-
20
- * The user may not state or imply any endorsement from the University of Minnesota or the GroupLens Research Group.
21
- * The user must acknowledge the use of the data set in publications resulting from the use of the data set (see below for citation information).
22
- * The user may redistribute the data set, including transformations, so long as it is distributed under these same license conditions.
23
- * The user may not use this information for any commercial or revenue-bearing purposes without first obtaining permission from a faculty member of the GroupLens Research Project at the University of Minnesota.
24
- * The executable software scripts are provided "as is" without warranty of any kind, either expressed or implied, including, but not limited to, the implied warranties of merchantability and fitness for a particular purpose. The entire risk as to the quality and performance of them is with you. Should the program prove defective, you assume the cost of all necessary servicing, repair or correction.
25
-
26
- In no event shall the University of Minnesota, its affiliates or employees be liable to you for any damages arising out of the use or inability to use these programs (including but not limited to loss of data or data being rendered inaccurate).
27
-
28
- If you have any further questions or comments, please email <grouplens-info@umn.edu>
29
-
30
-
31
- Citation
32
- ========
33
-
34
- To acknowledge use of the dataset in publications, please cite the following paper:
35
-
36
- > F. Maxwell Harper and Joseph A. Konstan. 2015. The MovieLens Datasets: History and Context. ACM Transactions on Interactive Intelligent Systems (TiiS) 5, 4: 19:1–19:19. <https://doi.org/10.1145/2827872>
37
-
38
-
39
- Further Information About GroupLens
40
- ===================================
41
-
42
- GroupLens is a research group in the Department of Computer Science and Engineering at the University of Minnesota. Since its inception in 1992, GroupLens's research projects have explored a variety of fields including:
43
-
44
- * recommender systems
45
- * online communities
46
- * mobile and ubiquitious technologies
47
- * digital libraries
48
- * local geographic information systems
49
-
50
- GroupLens Research operates a movie recommender based on collaborative filtering, MovieLens, which is the source of these data. We encourage you to visit <http://movielens.org> to try it out! If you have exciting ideas for experimental work to conduct on MovieLens, send us an email at <grouplens-info@cs.umn.edu> - we are always interested in working with external collaborators.
51
-
52
-
53
- Content and Use of Files
54
- ========================
55
-
56
- Formatting and Encoding
57
- -----------------------
58
-
59
- The dataset files are written as [comma-separated values](http://en.wikipedia.org/wiki/Comma-separated_values) files with a single header row. Columns that contain commas (`,`) are escaped using double-quotes (`"`). These files are encoded as UTF-8. If accented characters in movie titles or tag values (e.g. Misérables, Les (1995)) display incorrectly, make sure that any program reading the data, such as a text editor, terminal, or script, is configured for UTF-8.
60
-
61
-
62
- User Ids
63
- --------
64
-
65
- MovieLens users were selected at random for inclusion. Their ids have been anonymized. User ids are consistent between `ratings.csv` and `tags.csv` (i.e., the same id refers to the same user across the two files).
66
-
67
-
68
- Movie Ids
69
- ---------
70
-
71
- Only movies with at least one rating or tag are included in the dataset. These movie ids are consistent with those used on the MovieLens web site (e.g., id `1` corresponds to the URL <https://movielens.org/movies/1>). Movie ids are consistent between `ratings.csv`, `tags.csv`, `movies.csv`, and `links.csv` (i.e., the same id refers to the same movie across these four data files).
72
-
73
-
74
- Ratings Data File Structure (ratings.csv)
75
- -----------------------------------------
76
-
77
- All ratings are contained in the file `ratings.csv`. Each line of this file after the header row represents one rating of one movie by one user, and has the following format:
78
-
79
- userId,movieId,rating,timestamp
80
-
81
- The lines within this file are ordered first by userId, then, within user, by movieId.
82
-
83
- Ratings are made on a 5-star scale, with half-star increments (0.5 stars - 5.0 stars).
84
-
85
- Timestamps represent seconds since midnight Coordinated Universal Time (UTC) of January 1, 1970.
86
-
87
-
88
- Tags Data File Structure (tags.csv)
89
- -----------------------------------
90
-
91
- All tags are contained in the file `tags.csv`. Each line of this file after the header row represents one tag applied to one movie by one user, and has the following format:
92
-
93
- userId,movieId,tag,timestamp
94
-
95
- The lines within this file are ordered first by userId, then, within user, by movieId.
96
-
97
- Tags are user-generated metadata about movies. Each tag is typically a single word or short phrase. The meaning, value, and purpose of a particular tag is determined by each user.
98
-
99
- Timestamps represent seconds since midnight Coordinated Universal Time (UTC) of January 1, 1970.
100
-
101
-
102
- Movies Data File Structure (movies.csv)
103
- ---------------------------------------
104
-
105
- Movie information is contained in the file `movies.csv`. Each line of this file after the header row represents one movie, and has the following format:
106
-
107
- movieId,title,genres
108
-
109
- Movie titles are entered manually or imported from <https://www.themoviedb.org/>, and include the year of release in parentheses. Errors and inconsistencies may exist in these titles.
110
-
111
- Genres are a pipe-separated list, and are selected from the following:
112
-
113
- * Action
114
- * Adventure
115
- * Animation
116
- * Children's
117
- * Comedy
118
- * Crime
119
- * Documentary
120
- * Drama
121
- * Fantasy
122
- * Film-Noir
123
- * Horror
124
- * Musical
125
- * Mystery
126
- * Romance
127
- * Sci-Fi
128
- * Thriller
129
- * War
130
- * Western
131
- * (no genres listed)
132
-
133
-
134
- Links Data File Structure (links.csv)
135
- ---------------------------------------
136
-
137
- Identifiers that can be used to link to other sources of movie data are contained in the file `links.csv`. Each line of this file after the header row represents one movie, and has the following format:
138
-
139
- movieId,imdbId,tmdbId
140
-
141
- movieId is an identifier for movies used by <https://movielens.org>. E.g., the movie Toy Story has the link <https://movielens.org/movies/1>.
142
-
143
- imdbId is an identifier for movies used by <http://www.imdb.com>. E.g., the movie Toy Story has the link <http://www.imdb.com/title/tt0114709/>.
144
-
145
- tmdbId is an identifier for movies used by <https://www.themoviedb.org>. E.g., the movie Toy Story has the link <https://www.themoviedb.org/movie/862>.
146
-
147
- Use of the resources listed above is subject to the terms of each provider.
148
-
149
-
150
- Cross-Validation
151
- ----------------
152
-
153
- Prior versions of the MovieLens dataset included either pre-computed cross-folds or scripts to perform this computation. We no longer bundle either of these features with the dataset, since most modern toolkits provide this as a built-in feature. If you wish to learn about standard approaches to cross-fold computation in the context of recommender systems evaluation, see [LensKit](http://lenskit.org) for tools, documentation, and open-source code examples.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
notebooks/data/raw/tmdb5k/tmdb-movie-metadata/tmdb_5000_movies.csv DELETED
The diff for this file is too large to render. See raw diff
 
readme.txt DELETED
@@ -1,46 +0,0 @@
1
- Zona de comfort con la probabilidad de eleccion para cada genero. Ranking por probabilidad de eleccion
2
- Distancia = dentro del ranking
3
- Recomendar entre 3 y 5. Dentro de cada distnacia escojo las de mejor valoracion
4
- Umbral minimo de 40 de probabiliad de eleccion (un ou ms generos)
5
-
6
- Val del usuario --> Comparar vectores de recomendaciones de dos usuarios. En sistemas normales
7
-
8
- Usar como dimension adicional. Podemos ver que peliculas en X estado emocional y da valoraciones positivas.
9
- Si ademas de eleccion podemos conocer la val y el estado emocional en el que se encuentra podemos establecer una nueva estrategia.
10
-
11
-
12
-
13
-
14
-
15
-
16
-
17
-
18
- ---------------
19
-
20
- Si una pelicula ya contiene géneros de la zona de confort se pone a d=0? Como se gestiona esto
21
- --> Zona de confort no puede ser pertenece o no, grado de probabilidad, mirar porcentaje de pertenencia a la zona de confort. Elaborar distancias dentro de la zona de confort.
22
- Intensidad d la emocion define porcentaje a aceptar d la zona de confort
23
- Grado de confort que quiero en funcion de polaridad y valencia de la emocion
24
- Hacer un percentil basado en la distancia de un arosal 0
25
-
26
- Version 2 ranking ms fino basado en considerar el arosal o la intensidad de laa emocion, percentil
27
-
28
- Completar experimento personal
29
-
30
- Utilizar valoracion de recomendacion para sustituir nota global por ella cuando se obtenga
31
-
32
-
33
- FEITO DESDE A ANTERIOR REUNION:
34
- - en recomendar peliculas o ranking pasa de facerse segun conteo de generos a segun porcentaje sobre o total de peliculas
35
- e esto axuda a definir a zona de confort como os generos mais vistos que supoñan ms dun 40% de visionado do usuario (antes eran 2 mais vistos)
36
- - modificado recomendar_peliculas para ter un switch case en funcion d Version
37
- -v1 pasa a recomendar solo si e positiva ou negativa se esta dentro ou fora de zona d confort (clasificacion binaria)
38
- -v2, definense en emotion_service duas funcions (unha para calcular o arousal e outra para pasar a percentil)
39
- - calculo arousal usase uns pesos predeterminados por emocion, calcula a polaridad do texto usando o que nos devolve a ia e se normaliza
40
- - ese arousal pasase a un percentil, permitindo saber se a polaridad detectada é habitual no usuario ou non
41
- - ahora a maior arousal maior se busca sair da zona de confort se a emocion e positiva (definese unha funcion de forma que o arousal fai que pondere ms) e ao reves se e negativa
42
-
43
- -v3, pasamos de ver a lista de confort como un array de generos, ahora para cada pelicula hai un indice de confort entre 0 e 1.
44
- - con percentil desde cero, aseguramos q nn se sempre se esta estresado nn se teña en conta
45
- - se e positiva ou negativa, en funcion do arousal buscase un target de indice de confort a buscar nas peliculas
46
- - calculase o score como a distancia entre o target e o indice de confort da pelicula
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
requirements.txt CHANGED
@@ -7,6 +7,8 @@ python-dotenv==1.0.0
7
  # Modelo de emociones local (robertuito)
8
  transformers>=4.40.0
9
  torch>=2.1.0
 
 
10
  werkzeug>=2.3.0
11
 
12
  # Usadas en los Jupyter Notebooks
 
7
  # Modelo de emociones local (robertuito)
8
  transformers>=4.40.0
9
  torch>=2.1.0
10
+
11
+ #Seguridad
12
  werkzeug>=2.3.0
13
 
14
  # Usadas en los Jupyter Notebooks