Construyendo SmartDAM: un gestor de activos digitales con IA para fotografía gastronómica
Cómo desarrollé SmartDAM — una app Flask que analiza fotos de comida automáticamente vía HuggingFace, genera etiquetas multilingües, soporta Azure Blob Storage y ofrece búsqueda en tiempo real.
Por qué desarrollé SmartDAM
Gestionar una biblioteca de fotografía gastronómica para una demo de producto debería ser sencillo. En la práctica, nunca lo es.
Acabas con cientos de imágenes repartidas entre carpetas locales, buckets en la nube e hilos de Slack. Nadie sabe qué foto muestra una hamburguesa con lechuga, cuál está en orientación vertical o cuál ya se usó en la última campaña.
Desarrollé SmartDAM para resolver exactamente esto: un gestor de activos digitales diseñado para fotografía gastronómica que usa IA para analizar, etiquetar y hacer descubribles las imágenes automáticamente — sin ningún etiquetado manual.
El resultado es una aplicación Flask que permite importar imágenes, clasificarlas automáticamente vía la API HuggingFace Inference, almacenarlas localmente o en Azure Blob Storage, y encontrar cualquier cosa en milisegundos mediante búsqueda en tiempo real y filtros inteligentes.
El problema central: el etiquetado manual a escala
La solución obvia para descubrir imágenes son las etiquetas. La parte dolorosa es mantenerlas coherentes y completas en cientos de activos.
Los etiquetadores humanos son lentos, inconsistentes y caros de escalar. Las etiquetas que producen reflejan su vocabulario, no el tuyo. Y en el momento en que aparece una nueva categoría de imágenes — por ejemplo, "escenas de brunch al aire libre" — hay que re-etiquetar toda la biblioteca.
Mi respuesta: eliminar el etiquetado manual. Dejar que un modelo de visión vea cada imagen y produzca etiquetas estructuradas automáticamente.
Resumen de la arquitectura
SmartDAM es intencionalmente simple. El stack es:
| Capa | Tecnología |
|---|---|
| Framework web | Python / Flask |
| Base de datos | SQLite (consultas directas) |
| Procesamiento de imágenes | Pillow |
| Clasificación IA | HuggingFace Inference API |
| Almacenamiento | Sistema de archivos local o Azure Blob Storage |
| Frontend | Vanilla JS + Bootstrap 5 + templates Jinja2 |
Sin ORM. Sin React. Sin Webpack. El objetivo era lanzar algo rápido y mantener cada dependencia justificada.
Pipeline de importación de imágenes
Cuando un usuario sube una o varias imágenes, la aplicación las procesa mediante un pipeline de cuatro pasos:
1. Validación
Antes de tocar el almacenamiento, cada archivo pasa por Pillow para confirmar que es una imagen real y legible. Los uploads corruptos, los archivos vacíos y los falsos archivos no-imagen se rechazan inmediatamente con un mensaje de estado por archivo.
from PIL import Image
def validate_image(file_path: str) -> bool:
try:
with Image.open(file_path) as img:
img.verify()
return True
except Exception:
return False
Esto importa más de lo que parece. Sin esto, un upload incorrecto contamina silenciosamente la base de datos con registros rotos.
2. Generación de miniaturas
Se genera una miniatura del lado del servidor (320×240) para cada imagen en el momento de la importación. Las páginas de galería cargan miniaturas, no archivos de resolución completa. Esto mantiene la interfaz rápida independientemente del tamaño de los originales.
3. Almacenamiento
Las imágenes pueden ir al sistema de archivos local o a un contenedor de Azure Blob Storage — la aplicación lee STORAGE_BACKEND del entorno y enruta en consecuencia. La abstracción es una pequeña clase StorageBackend con métodos save() y url_for(), por lo que cambiar de backend no requiere ningún cambio en el pipeline de importación.
4. Análisis con IA
El último paso llama a la API HuggingFace Inference para clasificación de imágenes y detección de objetos. Aquí reside la inteligencia.
import requests
HF_API_URL = "https://api-inference.huggingface.co/models/google/vit-base-patch16-224"
def classify_image(image_bytes: bytes, api_key: str) -> list[dict]:
headers = {"Authorization": f"Bearer {api_key}"}
response = requests.post(HF_API_URL, headers=headers, data=image_bytes)
response.raise_for_status()
return response.json() # [{"label": "...", "score": 0.95}, ...]
La API devuelve una lista clasificada de etiquetas. SmartDAM toma los mejores resultados, los traduce al francés y los almacena como las etiquetas de la imagen. Si la API no está disponible, la aplicación degrada de forma elegante — la imagen se guarda igualmente y puede volver a analizarse más tarde con un solo clic.
Detección de personas
Una llamada separada al modelo comprueba si aparecen personas en la imagen. Esto rellena el booleano has_person en la base de datos, habilitando un filtro dedicado "personas" en la galería.
Diseño de la base de datos
SQLite mantiene las cosas simples. La tabla principal:
CREATE TABLE images (
id INTEGER PRIMARY KEY AUTOINCREMENT,
filename TEXT NOT NULL,
original_filename TEXT,
storage_url TEXT NOT NULL,
thumbnail_url TEXT,
tags TEXT, -- array JSON de cadenas
food_category TEXT,
environment TEXT, -- interior / exterior
orientation TEXT, -- vertical / horizontal / cuadrado
has_person INTEGER DEFAULT 0,
is_favorite INTEGER DEFAULT 0,
analyzed_at DATETIME,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
Las etiquetas se almacenan como un array JSON en una columna TEXT. Para la escala objetivo de SmartDAM (cientos a pocos miles de imágenes), esto es perfectamente apropiado y evita la complejidad de una tabla de unión. La búsqueda de texto completo se ejecuta en la cadena de etiquetas en memoria, no en un índice separado.
Búsqueda y filtrado en tiempo real
La galería usa filtrado del lado del cliente con un debounce de 400ms para no saturar el servidor con cada pulsación. En la entrada, una llamada fetch llega a /api/images?q=...&filters=..., y el JS re-renderiza solo las tarjetas que coinciden.
let debounceTimer;
searchInput.addEventListener("input", () => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
fetchAndRender({ q: searchInput.value, ...activeFilters });
}, 400);
});
Los filtros se acumulan: puedes buscar "ensalada" mientras también filtras por orientación horizontal, entorno exterior y sin personas. Cada filtro es una condición AND independiente.
El resaltado de términos de búsqueda se hace en el cliente. Cuando llegan los resultados, cualquier subcadena coincidente en el título de la tarjeta o la lista de etiquetas se envuelve en un elemento <mark> sin tocar la respuesta del servidor.
Barra de etiquetas frecuentes
Una fila fija encima de la galería muestra las 10 etiquetas más usadas como chips clicables. Hacer clic en un chip establece instantáneamente la consulta de búsqueda activa — útil para navegar por categoría sin escribir.
Seguridad XSS
Los datos proporcionados por el usuario (nombres de archivo originales, etiquetas traducidas desde una API externa) se renderizan en las tarjetas de la galería. Toda la salida pasa por el escape automático de Jinja2, y la función de resaltado del lado del cliente usa textContent en lugar de innerHTML al insertar fragmentos coincidentes.
Tema claro / oscuro
La preferencia de tema se almacena en localStorage y se aplica antes del primer renderizado mediante un pequeño script en línea en <head>. Esto evita el flash-of-wrong-theme que afecta a las implementaciones basadas únicamente en variables CSS.
<script>
const theme = localStorage.getItem("theme") || "light";
document.documentElement.setAttribute("data-theme", theme);
</script>
El atributo data-bs-theme de Bootstrap 5 hace el resto.
Favoritos
Cada imagen puede marcarse como favorita. El estado es un booleano is_favorite en la fila de la base de datos, alternado por un endpoint PATCH /api/images/:id/favorite. La galería tiene un filtro "Solo favoritos" que lo usa.
Lecciones aprendidas
1. Validar antes de almacenar
El orden del pipeline importa. Validar → miniatura → almacenar → analizar. Si almacenas primero y validas después, acabas limpiando archivos rotos del backend de almacenamiento en cada upload incorrecto.
2. La degradación elegante de la IA no es negociable
La API HuggingFace tiene límites de velocidad y caídas ocasionales. Cada ruta de importación necesita funcionar sin ella. Almacenar imágenes sin etiquetas y mostrar un botón "re-analizar" más tarde es mucho mejor que bloquear los uploads en una llamada API externa.
3. Mantener el esquema de base de datos simple hasta necesitar complejidad
Una columna JSON para las etiquetas evita una tabla de unión y tres consultas adicionales por cada carga de galería. La función json_each() de SQLite puede consultar dentro de ella si alguna vez se necesita. Empezar simple.
4. El debounce no es opcional
Sin el debounce de 400ms, un usuario que escribe rápido dispara 10-15 solicitudes fetch por segundo. La galería parpadearía y el servidor haría trabajo redundante constantemente. El debounce cuesta dos líneas de código y ahorra un dolor considerable.
5. Miniaturas en la importación, no en el renderizado
Generar miniaturas bajo demanda en la primera carga de la galería es tentador (sin almacenamiento extra), pero bloquea la primera solicitud y crea problemas de avalancha si se solicitan muchas imágenes simultáneamente. Generar una vez en la importación y servir estáticamente.
Próximos pasos
SmartDAM es actualmente una aplicación de un solo usuario. Los próximos pasos obvios son:
- Autenticación — cuentas de usuario para que varias personas puedan compartir la misma biblioteca sin pisarse.
- Re-análisis en lote — volver a ejecutar los modelos HuggingFace en todas las imágenes no analizadas en un worker en segundo plano.
- Mejor selección de modelos — el modelo ViT-base actual es bueno para clasificación general, pero un modelo especializado en gastronomía produciría etiquetas más útiles.
- Despliegue en producción — un
Dockerfiley un pipeline CI básico permitirían ejecutar esto en cualquier entorno cloud en minutos.
Conclusión
SmartDAM tomó aproximadamente una semana de tardes concentradas para alcanzar un estado utilizable. La combinación de la simplicidad de Flask, la fiabilidad de Pillow y la amplitud de la API HuggingFace Inference forma un stack sorprendentemente capaz para una herramienta visual con IA.
El código fuente está en GitHub: github.com/LMouhssine/SmartDAM. Si construyes algo encima o encuentras problemas, abre un issue — los leo.
Articulos relacionados
Cómo Construir Agentes IA con LangChain: Tutorial Completo 2026
Tutorial paso a paso para construir agentes IA listos para producción con LangChain. Desde setup hasta despliegue con herramientas, memoria, evaluación y manejo de errores.
Cómo funcionan realmente los agentes de IA: arquitectura, memoria, herramientas y el bucle del agente
Guía técnica sobre la arquitectura de un agente de IA: bucle del agente, herramientas, memoria (RAG/vector DB), evaluación y fallos comunes en producción.
Por qué fallan los agentes de IA (y cómo arreglarlos)
Guía práctica sobre fallas de agentes IA en producción y cómo corregirlas con objetivos claros, memoria, herramientas, evaluación, UX y seguridad.
