Spaces:
Running
Running
Separacion codigo e documentacion
Browse files- DIAGRAMAS_PATRONES.md +339 -0
- README.md +261 -0
- backend/aplicacion.py +124 -22
- backend/base_datos.py +4 -7
- backend/conexion_bd.py +0 -3
- backend/dao/ciclo_dao.py +1 -2
- backend/dao/emocion_dao.py +1 -2
- backend/dao/historial_dao.py +2 -2
- backend/dao/pelicula_dao.py +14 -22
- backend/dao/usuario_dao.py +4 -5
- backend/do.py +42 -0
- backend/{modelos.py → modelos_servicios.py} +1 -97
- backend/scripts/crear_bd.py +1 -4
- backend/scripts/limpiar_bdm.py +1 -4
- backend/services/calculos.py +7 -2
- backend/services/estrategias_recomendacion.py +1 -1
- backend/services/movielens_service.py +59 -0
- backend/services/pipeline.py +2 -1
- backend/services/recomendacion.py +1 -97
- backend/vo.py +34 -0
- chatbot/src/views/AuthView.vue +412 -135
- {notebooks/data/raw → data}/ml-latest-small/links.csv +0 -0
- {notebooks/data/raw → data}/ml-latest-small/movies.csv +0 -0
- {notebooks/data/raw → data}/ml-latest-small/ratings.csv +0 -0
- {notebooks/data/raw → data}/ml-latest-small/tags.csv +0 -0
- data/ml-latest/README.txt +0 -179
- data/{download_movielens_large.py → movielens.py} +4 -4
- data/procesado/peliculas_100_emociones.csv +0 -101
- data/procesado/peliculas_conocidas.csv +0 -21
- data/script.py +0 -174
- data/textos_complejo.csv +0 -43
- data/textos_simple.csv +0 -43
- docs/diagrama_er.md +9 -15
- docs/login_seq.md +28 -0
- notebooks/01_exploracion_apis.ipynb +0 -0
- notebooks/02_exploracion_datasets.ipynb +0 -0
- notebooks/data/raw/ml-latest-small/README.txt +0 -153
- notebooks/data/raw/tmdb5k/tmdb-movie-metadata/tmdb_5000_movies.csv +0 -0
- readme.txt +0 -46
- 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
|
| 22 |
from services.pipeline import AnalysisService
|
| 23 |
from services.analisis_sentimientos import analizar_texto, crear_clasificador_emociones
|
| 24 |
-
from services.
|
| 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
|
| 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
|
| 41 |
genres_raw = str(row.get("genres", "") or "").strip()
|
| 42 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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,
|
| 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
|
| 48 |
-
titulo
|
| 49 |
-
|
| 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
|
| 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
|
| 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
|
| 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
|
| 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,
|
| 13 |
-
genero: str | None = None
|
| 14 |
with self.obtener_conexion() as con:
|
| 15 |
con.execute(
|
| 16 |
-
"""INSERT INTO Peliculas (id, titulo,
|
| 17 |
-
VALUES (?, ?, ?
|
| 18 |
ON CONFLICT(id) DO UPDATE SET
|
| 19 |
-
|
| 20 |
-
|
| 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,
|
| 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,
|
| 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,
|
| 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,
|
| 53 |
-
genero: str | None = None
|
| 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
|
| 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,
|
| 25 |
-
VALUES (?, ?, ?, ?, ?
|
| 26 |
-
(user_id, nombre,
|
| 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 |
-
|
| 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 |
-
|
| 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
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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
|
|
|
|
| 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
|
| 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 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
<
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
<
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
<
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
variant="tonal"
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
</
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
v-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
variant="
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
<
|
| 127 |
-
v-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 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:
|
| 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 |
-
|
| 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
|
| 45 |
)
|
| 46 |
parser.add_argument(
|
| 47 |
"--url",
|
| 48 |
-
default=
|
| 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
|
| 54 |
-
Usuarios ||--o{ Historial_Peliculas
|
| 55 |
-
Usuarios ||--o{ Ciclo_Recomendacion
|
|
|
|
| 56 |
Peliculas ||--o{ Historial_Peliculas : "aparece en"
|
| 57 |
Peliculas ||--o{ Ciclo_Recomendacion : "recomendada en"
|
| 58 |
-
|
| 59 |
-
Emociones ||--o{
|
| 60 |
-
Emociones ||--o{ Ciclo_Recomendacion : "
|
| 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
|