File size: 11,685 Bytes
20acd0c
 
632ef52
c4b1914
632ef52
c4b1914
 
5df7fe4
c4b1914
 
 
 
fed3071
c4b1914
20acd0c
 
 
 
 
 
c4b1914
 
 
 
 
 
20acd0c
 
c4b1914
 
 
 
 
 
 
 
20acd0c
 
 
c4b1914
 
 
20acd0c
c4b1914
20acd0c
 
 
c4b1914
fed3071
 
 
 
 
c4b1914
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fed3071
c4b1914
 
 
fed3071
c4b1914
 
 
 
 
 
 
 
 
 
 
 
 
20acd0c
 
 
540937a
20acd0c
 
632ef52
c4b1914
fed3071
 
 
 
 
 
 
 
 
 
 
 
 
 
c4b1914
 
632ef52
 
 
540937a
 
 
 
 
 
fed3071
540937a
fed3071
 
 
 
 
632ef52
fed3071
c4b1914
 
 
 
 
 
 
 
632ef52
 
c4b1914
 
 
 
540937a
 
 
 
c4b1914
81fbbf6
c4b1914
 
 
 
 
 
 
 
 
29210fc
c4b1914
 
 
 
81fbbf6
 
c4b1914
81fbbf6
540937a
 
 
 
 
 
 
 
81fbbf6
 
 
 
 
540937a
81fbbf6
540937a
f74c751
540937a
d76f986
 
 
fed3071
 
d76f986
fed3071
 
d76f986
540937a
d76f986
fed3071
 
 
 
 
 
 
 
 
d76f986
c4b1914
540937a
 
 
 
fed3071
540937a
 
 
4c4ec3c
c4b1914
 
 
 
fed3071
c4b1914
 
 
 
 
4c4ec3c
 
 
 
 
 
d76f986
 
4c4ec3c
 
 
 
 
 
 
 
 
 
 
 
 
81fbbf6
 
 
 
c4b1914
fed3071
 
 
 
 
 
 
 
 
c4b1914
81fbbf6
c4b1914
2cf4e9b
632ef52
 
 
 
 
c4b1914
632ef52
 
 
 
c4b1914
81fbbf6
c4b1914
9eaaf51
c4b1914
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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
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
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
from datasets import load_dataset, concatenate_datasets, Dataset
from sentence_transformers import SentenceTransformer
import numpy as np
import pandas as pd
import gradio as gr
import kagglehub
from kagglehub import KaggleDatasetAdapter
import faiss
import os
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch
import unicodedata
import random


# ==================================================
# Cargar datasets
# ==================================================

# Dataset 1: MyDramaList (5000 dramas)
mydramalist = kagglehub.load_dataset(
    KaggleDatasetAdapter.PANDAS,
    "anittasaju/complete-5000-dramas-from-mydramalist-review-info",
    "Top_5000_popular_drama_details_from_mydramalist.csv"
)
mydramalist = Dataset.from_pandas(mydramalist)

# Dataset 2: Netflix Movies and Shows
netflix_movies_shows = load_dataset("harshi321/netflix-movies_shows")["train"]

# ==================================================
# Preprocesamiento de datos
# ==================================================

# Renombrar columnas para unificar los datasets
mydramalist = mydramalist.rename_column("name", "title").rename_column("content", "description")

# Filtrar por país (Corea del Sur)
def filter_kdramas(dataset):
    if 'country' in dataset.features:
        return dataset.filter(lambda x: x['country'] == "South Korea" if x['country'] else False)
    return dataset

# Aplicar filtro a los datasets
kdramas1 = filter_kdramas(netflix_movies_shows)
kdramas2 = filter_kdramas(mydramalist)

# Eliminar columnas innecesarias (incluyendo 'rating')
columns_to_remove = ["Unnamed: 0", "no_of_reviews", "aka_names", "screenwriter", "director",
                     "no_of_viewers", "end_date", "start_date", "year", "duration", "no_of_rating",
                     "rank", "popularity", "content_rating", "where_to_watch", "main_role",
                     "support_role", "no_of_extracted_reviews", "Total_sentences",
                     "POSITIVE_people_sentiment", "POSITIVE_sentences", "NEGATIVE_people_sentiment",
                     "NEGATIVE_sentences", "rating"]  # Eliminar 'rating'

for dataset in [kdramas1, kdramas2]:
    dataset = dataset.remove_columns([col for col in columns_to_remove if col in dataset.features])

# Asegurar que todos los datasets tengan la columna 'type'
def add_type_column(dataset, default_type="TV Show"):
    if 'type' not in dataset.features:
        dataset = dataset.map(lambda x: {"type": default_type})
    return dataset

kdramas1 = add_type_column(kdramas1, "TV Show")
kdramas2 = add_type_column(kdramas2, "TV Show")

# Asegurar que todos los datasets tengan la columna 'genres'
def add_genres_column(dataset, default_genres="Unknown"):
    if 'genres' not in dataset.features:
        dataset = dataset.map(lambda x: {"genres": default_genres})
    return dataset

kdramas1 = add_genres_column(kdramas1, "Unknown")
kdramas2 = add_genres_column(kdramas2, "Unknown")

# Asegurar que todos los datasets tengan las mismas columnas y tipos
def align_datasets(dataset1, dataset2):
    # Obtener las columnas comunes
    common_columns = set(dataset1.features.keys()).intersection(set(dataset2.features.keys()))

    # Mantener solo las columnas comunes
    dataset1 = dataset1.select_columns(list(common_columns))
    dataset2 = dataset2.select_columns(list(common_columns))

    return dataset1, dataset2

# Alinear los datasets
kdramas1, kdramas2 = align_datasets(kdramas1, kdramas2)

# Eliminar columnas adicionales que no se usan
kdramas1 = kdramas1.remove_columns(['rating'])
kdramas2 = kdramas2.remove_columns(['rating'])

# Combinar todos los datasets
kdramas = concatenate_datasets([kdramas1, kdramas2])

# Eliminar duplicados basados en el título
kdramas_df = kdramas.to_pandas().drop_duplicates(subset=['title'])
kdramas = Dataset.from_pandas(kdramas_df)

# ==================================================
# Modelo de embeddings y recomendación
# ==================================================

# Cargar el modelo de embeddings
model = SentenceTransformer('sentence-transformers/paraphrase-MiniLM-L6-v2')

# Calcular o cargar embeddings
# Verificar si el archivo de embeddings existe
if os.path.exists("kdrama_embeddings.npy"):
    # Cargar embeddings precalculados
    embeddings_np = np.load("kdrama_embeddings.npy")
else:
    # Calcular embeddings y guardarlos
    descriptions = kdramas["description"]
    embeddings = model.encode(descriptions, convert_to_tensor=True)
    embeddings_np = embeddings.cpu().numpy()
    np.save("kdrama_embeddings.npy", embeddings_np)
    print("¡Embeddings listos! Cada descripción ahora es un vector numérico.")

# Crear un índice FAISS para búsqueda eficiente
dimension = embeddings_np.shape[1]
index = faiss.IndexFlatL2(dimension)
index.add(embeddings_np)

# Función para recomendar K-Dramas similares
def recommend_kdramas(title, k=5):
    title_indices = [i for i, t in enumerate(kdramas['title']) if title.lower() in t.lower()]
    if not title_indices:
        return f"No se encontraron títulos similares a '{title}'."
    
    query_embedding = embeddings_np[title_indices[0]].reshape(1, -1)
    distances, similar_indices = index.search(query_embedding, k + 10)  # Ampliar el rango de búsqueda
    
    # Seleccionar aleatoriamente k índices de los 10 más similares
    selected_indices = random.sample(list(similar_indices[0][1:]), k)
    
    recommendations = []
    for i in selected_indices:
        recommended_title = kdramas["title"][i]
        recommended_type = kdramas["type"][i] if "type" in kdramas.features else "Unknown"
        recommended_genres = kdramas["genres"][i] if "genres" in kdramas.features else "Unknown"
        recommendations.append(
            f"### {recommended_title}\n"
            f"- **Tipo**: {recommended_type}\n"
            f"- **Géneros**: {recommended_genres}\n"
        )
    return "\n".join(recommendations)

# ==================================================
# Chatbot
# ==================================================

# Cargar el modelo de chat
tokenizer = AutoTokenizer.from_pretrained("microsoft/DialoGPT-medium")
model_chat = AutoModelForCausalLM.from_pretrained("microsoft/DialoGPT-medium")

# Mapeo de géneros (español sin tildes → inglés)
mapeo_generos = {
    # Géneros (sin tildes)
    "romantico": "romantic",
    "accion": "action",
    "misterio": "mystery",
    "comedia": "comedy",
    "drama": "drama",
    "crimen": "crime",
    "fantasia": "fantasy",
    "thriller": "thriller",
    "romance": "romance",
    # Palabras clave adicionales (sin tildes)
    "aventura": "adventure",
    "historico": "historical",
    # Agrega más según sea necesario
}

# Función para normalizar texto
def normalizar_texto(texto):
    # Eliminar tildes y convertir a minúsculas
    texto_normalizado = ''.join(
        letra for letra in unicodedata.normalize('NFD', texto)
        if unicodedata.category(letra) != 'Mn'
    )
    return texto_normalizado.lower()

# Función para traducir preferencias
def traducir_preferencia(entrada_usuario):
    entrada_normalizada = normalizar_texto(entrada_usuario)
    for espanol, ingles in mapeo_generos.items():
        if espanol in entrada_normalizada:
            return ingles
    return None  # Si no se encuentra una traducción

# Función para buscar K-Dramas por género
def buscar_por_genero(genero, k=5):
    # Filtrar K-Dramas que contengan el género especificado
    genre_embedding = model.encode(genero, convert_to_tensor=True).cpu().numpy()
    genre_embedding = genre_embedding.reshape(1, -1)

    # Buscar en el índice FAISS
    D, I = index.search(genre_embedding, k * 2)  # Ampliar el rango de búsqueda

    # Seleccionar aleatoriamente k índices de los resultados
    selected_indices = random.sample(list(I[0]), k)

    # Formatear las recomendaciones
    recommendations = []
    for i in selected_indices:
        recommended_title = kdramas["title"][i]
        recommended_type = kdramas["type"][i] if "type" in kdramas.features else "Unknown"
        recommended_genres = kdramas["genres"][i] if "genres" in kdramas.features else "Unknown"
        recommendations.append(
            f"{recommended_title}\n"
            f"- **Tipo**: {recommended_type}\n"
            f"- **Géneros**: {recommended_genres}\n"
        )
    return "\n".join(recommendations) if recommendations else f"No se encontraron K-Dramas del género '{genero}'."

# Función para recomendar K-Dramas basado en preferencias
def recomendar_kdramas_chat(entrada_usuario):
    # Traducir preferencia del usuario
    preferencia_traducida = traducir_preferencia(entrada_usuario)

    if preferencia_traducida:
        return buscar_por_genero(preferencia_traducida, k=5)
    else:
        return None  # No se encontró una preferencia válida

# Función para generar respuestas del chatbot
def generar_respuesta(entrada_usuario, historial_chat=""):
    inputs = tokenizer.encode(entrada_usuario + historial_chat, return_tensors="pt")
    respuesta_ids = model_chat.generate(inputs, max_length=1000, pad_token_id=tokenizer.eos_token_id)
    respuesta = tokenizer.decode(respuesta_ids[:, inputs.shape[-1]:][0], skip_special_tokens=True)
    return respuesta

# Función para manejar la interacción del chatbot
def chat(entrada_usuario, historial_chat=""):
    # Agregar el mensaje del usuario al historial
    historial_chat += f"Usuario: {entrada_usuario}\n"

    # Generar una respuesta natural del chatbot
    if not historial_chat.strip():  # Si es la primera interacción
        respuesta = "¡Hola! Soy tu asistente para recomendar K-Dramas. ¿Qué tipo de K-Drama te gustaría ver hoy?"
    else:
        respuesta = generar_respuesta(entrada_usuario, historial_chat)

        # Verificar si el usuario mencionó un género
        recomendacion = recomendar_kdramas_chat(entrada_usuario)
        if recomendacion:
            respuesta = f"¡Genial! Aquí tienes algunas recomendaciones de K-Dramas que podrían gustarte:\n{recomendacion}"
        else:
            respuesta = "¿Qué tipo de K-Drama te gustaría ver? Puedo recomendarte dramas de acción, romance, misterio y más."

    # Agregar la respuesta del chatbot al historial
    historial_chat += f"Chatbot: {respuesta}\n"

    # Devolver la respuesta y el historial actualizado
    return respuesta, historial_chat, ""  # Limpiar el cuadro de texto

# ==================================================
# Interfaz de Gradio
# ==================================================

# Interfaz para el chatbot
interfaz_chatbot = gr.Interface(
    fn=chat,
    inputs=[gr.Textbox(label="Escribe tu mensaje"), gr.Textbox(label="Historial", visible=False)],
    outputs=[gr.Textbox(label="Respuesta del chatbot"), gr.Textbox(label="Mensaje", visible=False)],
    title="Chatbot Recomendador de K-Dramas",
    description="Habla con el chatbot para obtener recomendaciones personalizadas de K-Dramas.",
    allow_flagging="never"
)
# Interfaz para el recomendador tradicional
interfaz_recomendador = gr.Interface(
    theme=gr.themes.Citrus(),
    fn=recommend_kdramas,
    inputs=[
        gr.Textbox(label="Ingresa el título de un K-Drama"),
    ],
    outputs=gr.Markdown(label="K-Dramas recomendados"),
    title="Recomendador de K-Dramas",
    description="Ingresa el título de un K-Drama y recibe recomendaciones similares, indicando si son series o películas y sus géneros.",
    examples=[["Vincenzo", 5], ["Crash Landing on You", 3], ["Itaewon Class", 4]],
    allow_flagging="never",
)

# Lanzar ambas interfaces
gr.TabbedInterface(
    [interfaz_recomendador, interfaz_chatbot],  # Chatbot en la segunda pestaña
    ["Recomendador", "Chatbot"]
).launch()