Construire SmartDAM : un gestionnaire d'actifs numériques propulsé par l'IA pour la photographie culinaire
Comment j'ai développé SmartDAM — une application Flask qui analyse automatiquement les photos de food via HuggingFace, génère des tags multilingues, supporte Azure Blob Storage et offre une recherche en temps réel.
Pourquoi j'ai développé SmartDAM
Gérer une photothèque culinaire pour une démonstration produit devrait être simple. En pratique, ça ne l'est jamais.
On se retrouve avec des centaines d'images éparpillées entre des dossiers locaux, des buckets cloud et des fils Slack. Personne ne sait quelle photo montre un burger avec de la laitue, laquelle est en orientation portrait, ni laquelle a déjà été utilisée dans la dernière campagne.
J'ai développé SmartDAM pour résoudre exactement ce problème : un gestionnaire d'actifs numériques taillé pour la photographie culinaire qui utilise l'IA pour analyser, taguer et rendre les images découvrables automatiquement — sans aucun étiquetage manuel.
Le résultat est une application Flask qui permet d'importer des images, de les classifier via l'API HuggingFace Inference, de les stocker localement ou dans Azure Blob Storage, et de retrouver n'importe quoi en millisecondes grâce à une recherche en temps réel et des filtres intelligents.
Le problème fondamental : le tagging manuel à l'échelle
La solution évidente pour découvrir des images, c'est les tags. La partie douloureuse, c'est de les maintenir cohérents et complets sur des centaines d'actifs.
Les tagueurs humains sont lents, inconsistants et coûteux à l'échelle. Les tags qu'ils produisent reflètent leur vocabulaire, pas le vôtre. Et dès qu'une nouvelle catégorie d'images apparaît — par exemple des "scènes de brunch en extérieur" — il faut re-taguer toute la bibliothèque.
Ma réponse : supprimer le tagging manuel. Laisser un modèle de vision voir chaque image et produire des étiquettes structurées automatiquement.
Vue d'ensemble de l'architecture
SmartDAM est volontairement simple. La stack est :
| Couche | Technologie |
|---|---|
| Framework web | Python / Flask |
| Base de données | SQLite (requêtes directes) |
| Traitement d'images | Pillow |
| Classification IA | HuggingFace Inference API |
| Stockage | Système de fichiers local ou Azure Blob Storage |
| Frontend | Vanilla JS + Bootstrap 5 + templates Jinja2 |
Pas d'ORM. Pas de React. Pas de Webpack. L'objectif était de livrer rapidement et de garder chaque dépendance justifiée.
Pipeline d'import des images
Quand un utilisateur importe une ou plusieurs images, l'application les fait passer par un pipeline en quatre étapes :
1. Validation
Avant de toucher au stockage, chaque fichier passe par Pillow pour confirmer que c'est bien une image lisible. Les uploads corrompus, les fichiers vides et les faux fichiers non-image sont rejetés immédiatement avec un message de statut par fichier.
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
C'est plus important qu'il n'y paraît. Sans ça, un mauvais upload pollue silencieusement la base de données avec des enregistrements cassés.
2. Génération de miniatures
Une miniature côté serveur (320×240) est générée pour chaque image à l'import. Les pages de galerie chargent des miniatures, pas des fichiers pleine résolution. Cela maintient l'interface rapide quelle que soit la taille des originaux.
3. Stockage
Les images peuvent aller dans le système de fichiers local ou dans un conteneur Azure Blob Storage — l'application lit STORAGE_BACKEND depuis l'environnement et route en conséquence. L'abstraction est une petite classe StorageBackend avec des méthodes save() et url_for(), donc changer de backend ne nécessite aucune modification du pipeline d'import.
4. Analyse IA
La dernière étape appelle l'API HuggingFace Inference pour la classification d'images et la détection d'objets. C'est là que réside l'intelligence.
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}, ...]
L'API retourne une liste classée d'étiquettes. SmartDAM prend les meilleurs résultats, les traduit en français et les stocke comme tags de l'image. Si l'API est indisponible, l'application se dégrade gracieusement — l'image est quand même sauvegardée et peut être ré-analysée plus tard en un clic.
Détection de personnes
Un appel de modèle séparé vérifie si des personnes apparaissent dans l'image. Cela alimente le booléen has_person dans la base de données, activant un filtre dédié "personnes" dans la galerie.
Conception de la base de données
SQLite garde les choses simples. La table principale :
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, -- tableau JSON de chaînes
food_category TEXT,
environment TEXT, -- intérieur / extérieur
orientation TEXT, -- portrait / paysage / carré
has_person INTEGER DEFAULT 0,
is_favorite INTEGER DEFAULT 0,
analyzed_at DATETIME,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
Les tags sont stockés comme un tableau JSON dans une colonne TEXT. Pour l'échelle ciblée par SmartDAM (centaines à quelques milliers d'images), c'est parfaitement approprié et évite la complexité d'une table de jointure. La recherche plein texte s'exécute sur la chaîne de tags en mémoire, pas sur un index séparé.
Recherche et filtrage en temps réel
La galerie utilise un filtrage côté client avec un debounce de 400ms pour ne pas saturer le serveur à chaque frappe. À la saisie, un appel fetch touche /api/images?q=...&filters=..., et le JS re-rend uniquement les cartes correspondantes.
let debounceTimer;
searchInput.addEventListener("input", () => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
fetchAndRender({ q: searchInput.value, ...activeFilters });
}, 400);
});
Les filtres s'accumulent : on peut chercher "salade" tout en filtrant par orientation paysage, environnement extérieur et absence de personnes. Chaque filtre est une condition AND indépendante.
La mise en surbrillance des termes de recherche est faite côté client. Quand les résultats arrivent, toute sous-chaîne correspondante dans le titre de la carte ou la liste de tags est enveloppée dans un élément <mark> sans toucher la réponse du serveur.
Barre de tags fréquents
Une ligne fixe au-dessus de la galerie affiche les 10 tags les plus utilisés sous forme de chips cliquables. Cliquer sur un chip définit instantanément la requête de recherche active — utile pour naviguer par catégorie sans taper.
Sécurité XSS
Les données fournies par l'utilisateur (noms de fichiers originaux, tags traduits depuis une API externe) s'affichent dans les cartes de la galerie. Toute sortie passe par l'échappement automatique de Jinja2, et la fonction de mise en surbrillance côté client utilise textContent au lieu de innerHTML lors de l'insertion de fragments correspondants.
Thème clair / sombre
La préférence de thème est stockée dans localStorage et appliquée avant le premier rendu via un petit script inline dans <head>. Cela évite le flash-of-wrong-theme qui affecte les implémentations basées uniquement sur les variables CSS.
<script>
const theme = localStorage.getItem("theme") || "light";
document.documentElement.setAttribute("data-theme", theme);
</script>
L'attribut data-bs-theme de Bootstrap 5 fait le reste.
Favoris
Chaque image peut être marquée comme favorite. L'état est un booléen is_favorite sur la ligne de base de données, basculé par un endpoint PATCH /api/images/:id/favorite. La galerie dispose d'un filtre "Favoris uniquement" qui l'utilise.
Leçons apprises
1. Valider avant de stocker
L'ordre du pipeline compte. Valider → miniature → stocker → analyser. Si on stocke en premier et valide ensuite, on finit par nettoyer des fichiers cassés du backend de stockage à chaque mauvais upload.
2. La dégradation gracieuse de l'IA est non négociable
L'API HuggingFace a des limites de débit et des pannes occasionnelles. Chaque chemin d'import doit fonctionner sans elle. Stocker des images sans tags et afficher un bouton "re-analyser" plus tard est bien préférable à bloquer les uploads sur un appel API externe.
3. Garder le schéma de base de données simple jusqu'à en avoir besoin
Une colonne JSON pour les tags évite une table de jointure et trois requêtes supplémentaires par chargement de galerie. La fonction json_each() de SQLite peut interroger dedans si nécessaire. Commencer simple.
4. Le debounce n'est pas optionnel
Sans le debounce de 400ms, un utilisateur qui tape vite déclenche 10 à 15 requêtes fetch par seconde. La galerie scintillerait et le serveur ferait un travail redondant en permanence. Le debounce coûte deux lignes de code et évite beaucoup de problèmes.
5. Miniatures à l'import, pas au rendu
Générer des miniatures à la demande lors du premier chargement de la galerie est tentant (pas de stockage supplémentaire), mais cela bloque la première requête et crée des problèmes de requêtes simultanées si beaucoup d'images sont demandées en même temps. Générer une fois à l'import et servir statiquement.
La suite
SmartDAM est actuellement une application mono-utilisateur. Les prochaines étapes évidentes sont :
- Authentification — comptes utilisateurs pour que plusieurs personnes puissent partager la même bibliothèque sans se marcher dessus.
- Ré-analyse en batch — relancer les modèles HuggingFace sur toutes les images non analysées dans un worker en arrière-plan.
- Meilleure sélection de modèles — le modèle ViT-base actuel est bon pour la classification générale, mais un modèle spécialisé en food produirait des tags plus utiles.
- Déploiement en production — un
Dockerfileet un pipeline CI de base permettraient de faire tourner ça dans n'importe quel environnement cloud en quelques minutes.
Conclusion
SmartDAM a pris environ une semaine de soirées concentrées pour atteindre un état utilisable. La combinaison de la simplicité de Flask, de la fiabilité de Pillow et de la portée de l'API HuggingFace Inference forme une stack surprenamment capable pour un outil visuel IA.
Le code source est sur GitHub : github.com/LMouhssine/SmartDAM. Si vous construisez quelque chose dessus ou rencontrez des problèmes, ouvrez une issue — je les lis.
Articles similaires
Comment Construire des Agents IA avec LangChain : Tutoriel Complet 2026
Tutoriel pas à pas pour construire des agents IA prêts pour la production avec LangChain. Du setup au déploiement avec outils, mémoire, évaluation et gestion d'erreurs.
Comment fonctionnent vraiment les agents IA: architecture, mémoire, outils, et boucle d'agent
Guide technique sur l'architecture d'un agent IA: boucle d'agent, outils, mémoire (RAG/vector DB), évaluation et principaux modes d'échec en production.
Pourquoi les agents IA échouent (et comment les corriger)
Guide pratique sur les échecs des agents IA en production et les solutions: objectifs clairs, mémoire, outils, évaluation, UX et sécurité, pour équipes tech.
