Machine learning · Búsqueda vectorial

Kadabra: un Shazam para películas, sin forma de saber si funcionaba.

En 2023 construimos Kadabra, un bot de Telegram que intenta adivinar qué película estás viendo a partir de una foto de la pantalla o de unas líneas sobre la trama. Reconocía las caras de los actores, estimaba en qué año se tomó la foto a partir de su edad aparente y buscaba sinopsis por significado. Tres años después volví a leer el código. La arquitectura sigue en pie. Lo que nunca tuvo fue una forma de saber si sus respuestas eran correctas.

En resumen

Kadabra combinaba tres modelos detrás de un bot de Telegram: reconocimiento facial para encontrar a los actores en una foto, un estimador de edad para adivinar el año de la escena y un codificador de oraciones para buscar sinopsis por significado, todo guardado y consultado en Elasticsearch. Tareas de Celery recolectaban películas, repartos y fotos de actores en segundo plano. Al leerlo de nuevo, la ingeniería es sólida, pero el bot salió sin conjunto de prueba, sin pruebas y con umbrales elegidos a mano. Además, el texto de la consulta se preparaba distinto del texto indexado, los puntajes de búsquedas diferentes se sumaban y el mensaje de "sin resultados" nunca podía aparecer. Este artículo es una revisión de código, no una reconstrucción: no tengo cifras de precisión del bot, porque nunca produjo ninguna. Código: GitHub.

La idea

Todos hemos visto una escena en la pantalla de un amigo, o un fragmento en redes sociales, y nos hemos preguntado de qué película era. Shazam resolvió eso para las canciones. Kadabra, que el bot presentaba a los usuarios como FilmSleuth, fue nuestro intento para películas: le envías una foto de la pantalla, una descripción de la trama o ambas, y responde con hasta tres candidatas, su año de estreno y un enlace a IMDb. El comando /list muestra las películas que conoce.

2 entradasuna foto de la pantalla, una descripción de la trama o ambas
128 + 512dimensiones de los vectores de caras y de texto guardados en Elasticsearch
0pruebas, ejemplos etiquetados o mediciones de precisión en el repositorio

Fue un proyecto colaborativo. Ivan Zepeda creó el repositorio en mayo de 2023; el código que reviso aquí es la parte que yo subí entre junio y julio de 2023.

Cómo funcionaba

Kadabra es una aplicación de Django 4.2 con dos mitades: tareas en segundo plano que construyen los índices de búsqueda y un webhook que responde a Telegram.

Recolectar los datos. Las películas vivían en PostgreSQL y se administraban desde el admin de Django. Un calendario de Celery beat ejecutaba tareas cada cinco minutos. Una traía los metadatos de cada película desde la API de OMDb, y el reparto completo y la sinopsis desde IMDb, y creaba un registro por cada actor. Otra traía la galería de fotos de cada actor desde IMDb y solo guardaba retratos útiles: cuatro clasificadores Haar de OpenCV tenían que coincidir (casi) en que la imagen mostraba una sola cara, y la biblioteca face_recognition también tenía que encontrar exactamente una. Todas las tareas compartían una pequeña máquina de estados (lista, en ejecución, completada, error, expirada) con conteo de reintentos, y una tarea nocturna, escrita pero desactivada en el calendario, podía reactivar trabajos atascados "en ejecución" por más de un día.

Indexar caras. Para cada foto válida, face_recognition generaba una codificación de la cara de 128 números. Luego venía la parte que todavía me gusta: DeepFace estimaba qué edad aparentaba el actor en la foto. Si sumas esa edad a la fecha de nacimiento del actor, tomada de IMDb, obtienes una estimación del año en que se tomó la foto. Si en pantalla aparece un Keanu Reeves que aparenta 30 años, la película probablemente es de alrededor de 1994, no de 2021. La codificación, la edad y el año estimado se guardaban en un índice de Elasticsearch.

Indexar sinopsis. La trama, el título y el director de cada película se convertían en un vector de 512 números con el Universal Sentence Encoder de Google (versión large 5, de TensorFlow Hub) y se guardaban en un segundo índice, junto con los IDs de su reparto.

Responder. Cuando llega una foto, el bot codifica cada cara y busca la cara guardada más cercana a cada una, y conserva las coincidencias con una similitud coseno mayor a 0.93. Cuando llega texto, lo limpia con NLTK, lo codifica con el mismo modelo y hace una búsqueda de vecinos más cercanos sobre las sinopsis. Si llegan ambos, una sola consulta a Elasticsearch los combina: similitud semántica con la trama, más un impulso para las películas con un actor reconocido o estrenadas dentro de diez años del año estimado de la foto. Los umbrales, el número de resultados y la ventana de años se pueden editar en el admin con django-constance, y la lista de películas se guarda en caché en Redis.

Tres modelos, dos índices vectoriales, una cola de tareas y una interfaz de chat: las partes difíciles estaban todas. La parte barata, una lista de fotos con su respuesta correcta, nunca existió.

Al volver: lo que encontré

Al leer el código en 2026, la mayoría de los problemas no estaban en un modelo en particular. Estaban en los puntos donde los modelos se encuentran.

  • Nada medía si funcionaba. El repositorio no tiene ejemplos etiquetados (capturas o descripciones con su película correcta), y los dos archivos tests.py son las plantillas vacías de Django. El umbral de caras de 0.93 y el de resultados de 0.30 se eligieron probando el bot y ajustando. Sin un conjunto de prueba, cada cambio de umbral, de modelo o de consulta era una suposición, y también lo era cada demostración.
  • La pregunta no se parecía a los documentos. Las sinopsis se indexaban tal como estaban escritas. El texto del usuario se pasaba a minúsculas, se le quitaban números y palabras vacías, se lematizaba y luego pasaba por set(), que elimina palabras repetidas y también pierde su orden. Un codificador entrenado con oraciones naturales tenía que comparar bolsas de palabras del tipo "hombre encontrar anillo señor oscuro destruir" con párrafos reales. El texto con el que buscas debe prepararse exactamente igual que el texto que indexas.
  • Se sumaban puntajes de búsquedas distintas. Elasticsearch suma el puntaje de vecinos más cercanos al de la consulta normal. La consulta normal tenía una cláusula opcional por cada actor reconocido y otra por cada ventana de años, así que una película de la década correcta coincidía aunque no saliera ninguno de los actores. Después se aplicaba un único umbral de 0.30 a la suma de dos puntajes en escalas distintas.
  • La respuesta de "sin resultados" nunca podía aparecer. La función que da formato a los resultados siempre agrega una línea de "Best Result", así que nunca devuelve una respuesta vacía. Cuando nada superaba el umbral, el bot respondía "Best Result: None" en vez de su mensaje amable de "no hay suficiente información".
  • La búsqueda de caras comparaba contra todas las fotos. Cada cara se buscaba con una consulta script_score sobre todos los documentos, que calcula la similitud coseno contra cada cara guardada. Eso funciona con unos miles de fotos y es lento con millones. Los vectores de caras tampoco se configuraron para búsqueda aproximada de vecinos, aunque los de sinopsis sí. Y face_recognition está pensado para distancia euclidiana, con 0.6 como tolerancia habitual. La similitud coseno puede funcionar, pero había que encontrar el umbral de nuevo, sin datos para encontrarlo.
  • Un parche de 129 líneas para una línea equivocada. El descargador de páginas hacía str(resp.read()). En Python 3 eso no decodifica la página: devuelve la forma de texto de los bytes, b'...', con cada carácter acentuado escrito como un escape del tipo \xc3\xa9 y cada salto de línea como un \n literal. El código necesitó luego 129 llamadas a replace para convertir los escapes otra vez en letras, y un atributo de imagen que había que leer como "\nsrc" en vez de "src". Decodificar los bytes, resp.read().decode("utf-8"), habría hecho innecesario todo eso.
  • Extracción web que se rompe con el siguiente despliegue. Los analizadores de IMDb buscaban nombres de clases CSS generados, como sc-dec7a8b-2 haviXP, que cambian cada vez que se reconstruye el sitio, y rotaban agentes de usuario de navegador al azar. Una lista de 334 etiquetas de versiones de torrent (317 únicas, de "DVDRip" a "YIFY") limpiaba los nombres de las películas, y la detección del año solo buscaba años de 1900 a 2013. Las condiciones de uso de IMDb también restringen la recolección automatizada, así que una fuente con licencia, como los conjuntos de datos del propio IMDb o una API, es la base correcta para algo más que un proyecto académico.
  • Problemas operativos más pequeños. El codificador de oraciones, un modelo grande, se cargaba desde settings.py, así que cada comando de manage.py y cada worker de Celery lo cargaba, incluso los que nunca codifican texto. Un modelo de detección de edad de 5 MB (age_detect_cnn_model.h5) está en el repositorio pero nunca se usa: el código llama a DeepFace. La señal de borrado de los documentos de películas se copió de la de actores y hace referencia a un campo que las películas no tienen. Los errores se imprimían y se ignoraban, así que el bot respondía "There was an error" sin nada en los registros que explicara por qué.

Nada de esto es raro. He visto cada uno de estos problemas en sistemas en producción: preprocesamiento distinto entre entrenamiento y uso, puntajes combinados sin cuidado, un camino de resultado vacío que nadie probó y un error de decodificación parchado más adelante en vez de corregido en el origen.

Lo que resistió

  • El diseño del pipeline. Tareas pequeñas y reintentables con estados explícitos, una expiración para los fallos repetidos y un reinicio para el trabajo atascado es el mismo patrón que usaría hoy para cualquier proceso de recolección lento e inestable.
  • Un solo almacén para vectores y filtros. Guardar embeddings, IDs del reparto y años en Elasticsearch permitió mezclar búsqueda semántica y filtros exactos en una sola consulta, en vez de unir resultados de sistemas separados en Python.
  • La configuración fuera del código. Los umbrales, el número de resultados y los tiempos de caché se podían cambiar desde el admin sin desplegar. Es exactamente lo que necesitas mientras ajustas, siempre que también tengas una forma de medir el efecto de cada cambio.
  • Usar la edad como señal. Estimar el año de una escena a partir de la edad aparente de un actor es una forma barata y original de acotar una búsqueda entre décadas de películas. Merecía su propia evaluación.

Lo que haría distinto

No he reconstruido Kadabra. Si lo hiciera, este sería el orden:

  • Empezar por el conjunto de prueba. Unos cientos de capturas y descripciones de trama, cada una etiquetada con su película correcta, y un número a seguir: con qué frecuencia la película correcta queda entre las tres primeras. Cada decisión siguiente se verificaría contra él.
  • Preparar consultas y documentos igual. Enviar el texto del usuario al codificador tal como está escrito, igual que las sinopsis, y dejar que el modelo maneje las palabras vacías y las formas de las palabras.
  • Combinar rankings, no puntajes crudos. Ejecutar por separado las búsquedas de caras, años y texto y fusionar sus listas ordenadas, por ejemplo con reciprocal rank fusion, para que ninguna escala domine. La ventana de años debería ser un filtro o un impulso suave, nunca una coincidencia por sí sola.
  • Indexar las caras para búsqueda de vecinos. Configurar los vectores de caras para kNN aproximado, como las sinopsis, y ajustar el umbral con el conjunto de prueba.
  • Reconocer la escena, no solo las caras. Muchos cuadros no tienen una cara clara. Un modelo de embeddings de imagen y texto podría comparar el cuadro completo con fotogramas y descripciones, y también funcionar con animación.
  • Usar datos con licencia y probar la plomería. Reemplazar la extracción web por un conjunto de datos o una API con licencia, decodificar el texto una sola vez al recibirlo y agregar pruebas unitarias para el formato y el camino de resultado vacío, que habrían detectado la respuesta "Best Result: None".

Lo que aprendí

  • El conjunto de evaluación es parte del producto. Sin él, un sistema de machine learning es una demostración, por buena que sea su arquitectura.
  • La mayoría de los errores viven entre componentes. Cada modelo funcionaba. Los problemas estaban en un preprocesamiento que no coincidía, en puntajes que no se podían mezclar y en un camino de respuesta que nadie ejercitó.
  • Corrige los problemas de datos en el origen. Una línea de decodificación equivocada produjo un parche de 129 líneas y un nombre de atributo extraño. Cuando una función de limpieza no para de crecer, mira hacia atrás.
  • Las señales simples y originales merecen medirse. La idea de pasar de la edad al año fue lo más interesante del proyecto y lo que menos evidencia tenía detrás.

Dónde encajan los asistentes de IA

Como en mis revisiones de un clasificador de marketing bancario y de un modelo de visitas para Kijiji, usé un asistente de programación con IA para volver a leer el código de 2023 y seguir el camino de una foto o una frase desde Telegram hasta Elasticsearch y de regreso. Fue rápido para explicar por qué existían 129 llamadas a replace y para notar que el mensaje de resultado vacío era inalcanzable. Decidir cuáles de estos problemas importaban, y cómo sería una evaluación real, siguió siendo mi trabajo.

El proyecto y el código

Proyecto (2023)
Kadabra, un bot de Telegram (presentado a los usuarios como FilmSleuth) que identifica una película a partir de una foto de la pantalla o una descripción de la trama
Equipo
Repositorio colaborativo creado por Ivan Zepeda; el código del bot, del pipeline de datos y de la búsqueda lo subió Erwin Mongui
Modelos
face_recognition (codificaciones de caras de 128 dimensiones), estimación de edad con DeepFace, Universal Sentence Encoder large v5 (vectores de texto de 512 dimensiones), clasificadores Haar de OpenCV para filtrar fotos
Tecnologías
Python, Django 4.2, Celery, Redis, PostgreSQL, Elasticsearch 8, TensorFlow, NLTK, python-telegram-bot
Evaluación
Ninguna en el repositorio: sin ejemplos etiquetados, sin pruebas, umbrales elegidos a mano

Erwin MonguiDirector de Tecnología en Cedar Planters e ingeniero certificado en Inteligencia Artificial (Lambton College, 2024), en Toronto.

Recibe mis próximos casos y artículos en la Búsqueda de Google.