A release-readiness QA pass over the whole product. The commits split into defects a user would hit and gates that were reporting green while measuring nothing. ## Fixes that change behaviour Rate limiting was bypassable on every install: TRUST_PROXY defaulted to true, so request.ip came from a client-set header and a forged X-Forwarded-For got past the login limiter. The default is now a private-network trust list. A transient Postgres outage stranded in-flight jobs, leaving finished output on disk with no row pointing at it. A reconciler now resolves those rows and adopts the bytes rather than dropping the work. A Redis connection that moved to a new address wedged every read-blocked consumer, so completions stopped signalling while health still answered 200. Socket timeouts plus subscriber pings recover it. Installing more than one AI bundle left the shared venv multi-versioned and silently broke three tools. The installer now reconciles distributions to one version each. Converting an image to JXL at quality 1 through 4 returned a 500, because libjxl 0.7 rejects the distance those values compute. The quality is floored at what the encoder honours. A missing ffmpeg was also reported to the user as a corrupt upload; it now says the engine is unavailable. RAW uploads reached an unpatched LibRaw on arm64, so it is built from source at 0.22.2, and the release scan was split so it can fail on an unfixed critical instead of hiding it behind ignore-unfixed. ## Gates that could not fail Two mutation lanes ran zero mutants because Stryker crawled the gitignored docs build; coverage discarded its whole report on any failing test; the lint gate skipped root tests, scripts, and two workspaces; and several generated matrices counted a host missing ffmpeg as a passing tool. Each now measures what it claims. Full evidence and the outstanding release items are tracked locally and are not part of this branch.
10 KiB
description, i18n_source_hash, i18n_provenance, i18n_output_hash, i18n_hash_version
| description | i18n_source_hash | i18n_provenance | i18n_output_hash | i18n_hash_version |
|---|---|---|---|---|
| Structure du monorepo, architecture des applications et des packages, cycle de vie des requêtes et empreinte de ressources de SnapOtter. | 50e076925c4b | human | 86a7a8899e03 | 2 |
Architecture
SnapOtter est un monorepo géré avec les espaces de travail pnpm et Turborepo. Il se déploie sous forme de pile Docker Compose à 3 conteneurs : l'image de l'application SnapOtter, PostgreSQL 17 et Redis 8.
Structure du projet
snapotter/
├── apps/
│ ├── api/ # Fastify backend
│ ├── web/ # React + Vite frontend
│ └── docs/ # This VitePress site
├── packages/
│ ├── image-engine/ # Sharp-based image operations
│ ├── media-engine/ # FFmpeg spawn + progress parsing
│ ├── doc-engine/ # qpdf, LibreOffice, ghostscript wrappers
│ ├── ai/ # Python AI model bridge
│ └── shared/ # Types, constants, i18n
└── docker/ # Dockerfile and Compose config
Packages
@snapotter/image-engine
La bibliothèque principale de traitement d'images construite sur Sharp. Elle gère toutes les opérations sans IA : redimensionnement, rognage, rotation, retournement, conversion, compression, suppression des métadonnées et ajustements de couleur (luminosité, contraste, saturation, niveaux de gris, sépia, inversion, canaux de couleur).
Ce package n'a aucune dépendance réseau et s'exécute entièrement en cours de processus.
@snapotter/ai
Une couche de pont qui appelle les environnements d'exécution natifs et Python ML. La plupart des outils Python utilisent un dispatcher persistant qui pré-importe des bibliothèques lourdes (PIL, NumPy, MediaPipe, rembg), de sorte que les appels ultérieurs ignorent la surcharge d'importation. OCR est isolé de cet environnement partagé mutable : fast invoque Tesseract natif, tandis que balanced et best utilisent un JSONL persistant dédié dispatcher épinglé à la génération RapidOCR/ONNX active et immuable. Chaque requête contient un generation lease. L'activation exécute d'abord un smoke test sur un candidat, puis passe atomiquement à son dispatcher. Le dispatcher précédent est drainé avant que sa génération ne soit récupérée.
Les modèles ne sont pas préchargés. Chaque script d'outil charge les poids de son modèle depuis le disque au moment de la requête et les libère une fois la requête terminée. Consultez Empreinte de ressources pour le profil mémoire complet.
Opérations prises en charge : suppression de l'arrière-plan (rembg/BiRefNet), mise à l'échelle (RealESRGAN), flou du visage (MediaPipe), amélioration du visage (GFPGAN/CodeFormer), effacement d'objets (LaMa ONNX), OCR (Tesseract et RapidOCR avec les modèles PP-OCR ONNX), colorisation (DDColor), suppression du bruit, suppression des yeux rouges, restauration de photos, génération de photos d'identité, correction de la transparence. (BiRefNet HR-matting) et redimensionnement sensible au contenu (binaire Go caire).
Les scripts Python résident dans packages/ai/python/. De grands packs de modèles facultatifs sont installés à la demande dans le volume persistant /data/ai. Accurate OCR utilise des artefacts signés et spécifiques à la plate-forme ; le niveau Tesseract intégré ne nécessite aucun téléchargement de pack de modèles.
@snapotter/shared
Types TypeScript partagés, constantes (comme APP_VERSION et les définitions d'outils) et chaînes de traduction i18n utilisées à la fois par le frontend et le backend.
Applications
API (apps/api)
Un serveur Fastify v5 exposant 243 routes d'outils réparties sur cinq modalités (image, vidéo, audio, PDF, fichier) qui gère :
- Les téléversements de fichiers, la gestion de l'espace de travail temporaire et le stockage persistant des fichiers
- Bibliothèque de fichiers utilisateur (table
user_files) : une modification enregistrée est stockée par défaut comme un nouveau fichier indépendant, ou comme une version liée à son parent lorsque vous écrasez l'original. Elle enregistre les outils appliqués (toolChain) et obtient une vignette auto-générée pour la page Fichiers - L'exécution des outils (achemine chaque requête d'outil vers le moteur d'images ou le pont d'IA)
- L'orchestration de pipelines (enchaînement séquentiel de plusieurs outils)
- Le traitement par lots avec contrôle de la concurrence via les files d'attente de tâches BullMQ (pools : image, media, ai, docs, system)
- L'authentification des utilisateurs, le RBAC (rôles admin/user avec un ensemble complet de permissions), la gestion des clés d'API et la limitation de débit
- La gestion des équipes - CRUD réservé aux admins ; les utilisateurs sont affectés à une équipe via le champ
teamde leur profil - Les paramètres d'exécution - un magasin clé-valeur dans la table
settingsqui contrôledisabledTools,enableExperimentalTools,loginAttemptLimitet d'autres réglages opérationnels sans redéploiement - L'image de marque personnalisée et les préférences d'exécution via des paramètres stockés en base de données
- La documentation Scalar/OpenAPI à
/api/docs - La distribution du frontend compilé sous forme de SPA en production
Dépendances clés : Fastify, Drizzle ORM (pg-core, node-postgres), Sharp, BullMQ, ioredis, Zod pour la validation.
Le serveur gère l'arrêt gracieux sur SIGTERM/SIGINT : il draine les connexions HTTP, arrête les workers BullMQ, arrête le dispatcher Python et ferme la connexion à la base de données.
Web (apps/web)
Une application monopage React 19 construite avec Vite. Utilise Zustand pour la gestion de l'état, Tailwind CSS v4 pour le style et Lucide pour les icônes. Communique avec l'API via REST et SSE (pour le suivi de la progression).
Les pages comprennent un espace de travail d'outils, une page Fichiers pour gérer les téléversements et résultats persistants, un constructeur d'automatisation/pipeline, et un panneau de paramètres d'administration.
Le frontend compilé est distribué par le backend Fastify en production, il n'y a donc pas de serveur web distinct dans le conteneur Docker.
Docs (apps/docs)
Ce site VitePress. Déployé automatiquement sur Cloudflare Pages à chaque push sur main.
Cheminement d'une requête
- L'utilisateur choisit un outil dans l'interface web et téléverse un fichier.
- Le frontend envoie une requête POST multipart à
/api/v1/tools/:section/:toolIdavec le fichier et les paramètres. - La route de l'API valide l'entrée avec Zod, puis lance le traitement.
- Pour les outils standards, la tâche est mise en file d'attente dans le pool BullMQ approprié (image, media ou docs selon la modalité). Le worker BullMQ en cours de processus oriente automatiquement l'image d'après les métadonnées EXIF, exécute la fonction de traitement de l'outil et renvoie le résultat.
- Pour la plupart des outils d'IA, le pont TypeScript envoie une requête au Python dispatcher persistant. OCR rapide appelle à la place Tesseract, et OCR précis démarre l'exécutable épinglé à partir de la génération OCR immuable active. Le niveau OCR demandé est fixé lors de l’entrée et n’est jamais modifié silencieusement pendant l’exécution.
- La progression de la tâche est persistée dans la table
jobsde PostgreSQL afin que l'état survive aux redémarrages du conteneur. Les mises à jour en temps réel sont livrées via SSE à/api/v1/jobs/:jobId/progress. - L'API renvoie un
jobIdet unedownloadUrl. L'utilisateur télécharge le fichier traité depuis/api/v1/download/:jobId/:filename.
Pour les pipelines, l'API alimente l'étape suivante avec la sortie de chaque étape, en les exécutant séquentiellement.
Pour le traitement par lots, l'API utilise des flux BullMQ avec des tâches enfants par étape et renvoie un fichier ZIP contenant tous les fichiers traités.
Empreinte de ressources
SnapOtter est conçu pour une faible utilisation de mémoire au repos. Rien n'est préchargé ni maintenu chaud au démarrage.
Au repos
Le processus Node.js/Fastify, PostgreSQL et Redis sont en cours d'exécution. La RAM typique au repos est de ~200 à 300 Mo répartie sur les trois conteneurs (processus Node.js, Postgres et Redis). Aucun processus Python, aucun poids de modèle en mémoire.
Ce qui démarre, et quand
| Composant | Démarre quand | Mémoire pendant l'activité |
|---|---|---|
| Serveur Fastify + Postgres + Redis | Démarrage du conteneur | ~200 à 300 Mo au total |
| Workers BullMQ | Démarrage du conteneur (en cours de processus) | Un worker par pool (image, media, ai, docs, system) |
| Dispatcher Python | Première requête d'outil d'IA | Interpréteur Python + bibliothèques pré-importées (PIL, NumPy, MediaPipe, rembg) - aucun poids de modèle |
| Poids des modèles d'IA | Pendant la requête de l'outil concerné | Chargés depuis le disque, libérés à la fin de la requête |
Chargement des modèles
Tous les fichiers de poids des modèles (totalisant plusieurs Go) résident en permanence sur le disque dans /opt/models/. Chaque script d'outil d'IA charge en mémoire uniquement son ou ses modèles pour la durée d'une requête, puis les libère. Certains scripts appellent explicitement del model et torch.cuda.empty_cache() après l'inférence pour garantir la restitution immédiate de la mémoire.
Il n'y a pas de cache de modèles entre les requêtes. Exécuter le même outil d'IA de manière consécutive recharge le modèle à chaque fois. Cela maintient la mémoire au repos proche de zéro au prix d'un délai de chargement du modèle à chaque requête d'IA.
Démarrage à froid de la première requête d'IA
Le dispatcher Python n'est pas en cours d'exécution au démarrage du conteneur. La première requête d'IA déclenche deux choses en parallèle : le dispatcher commence à se préchauffer en arrière-plan, et la requête elle-même se rabat sur le lancement ponctuel d'un sous-processus Python. Une fois que le dispatcher signale qu'il est prêt, toutes les requêtes d'IA suivantes l'utilisent directement et évitent le coût du lancement d'un sous-processus.