Python
Flask
IA
HuggingFace
Gestión de activos digitales
Azure
Proyecto personal

Construyendo SmartDAM: un gestor de activos digitales con IA para fotografía gastronómica

Mouhssine Lakhili profile
Mouhssine Lakhili
27 de marzo de 20268 min de lectura

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.

Construyendo SmartDAM: un gestor de activos digitales con IA para fotografía gastronómica
Imagen de portada: Caso de estudio de SmartDAM, un gestor de activos digitales impulsado por IA.

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:

CapaTecnología
Framework webPython / Flask
Base de datosSQLite (consultas directas)
Procesamiento de imágenesPillow
Clasificación IAHuggingFace Inference API
AlmacenamientoSistema de archivos local o Azure Blob Storage
FrontendVanilla 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 Dockerfile y 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.

Compartir este artículo

Articulos relacionados