Files
SnapOtter/apps/docs/fr/guide/architecture.md
T
SnapOtterandGitHub d10d0f544f fix: release QA hardening across processing, media, security, and CI gates (#649)
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.
2026-07-27 15:37:30 +08:00

10 KiB
Raw Blame History

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 team de leur profil
  • Les paramètres d'exécution - un magasin clé-valeur dans la table settings qui contrôle disabledTools, enableExperimentalTools, loginAttemptLimit et 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

  1. L'utilisateur choisit un outil dans l'interface web et téléverse un fichier.
  2. Le frontend envoie une requête POST multipart à /api/v1/tools/:section/:toolId avec le fichier et les paramètres.
  3. La route de l'API valide l'entrée avec Zod, puis lance le traitement.
  4. 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.
  5. 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 lentrée et nest jamais modifié silencieusement pendant lexécution.
  6. La progression de la tâche est persistée dans la table jobs de 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.
  7. L'API renvoie un jobId et une downloadUrl. 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.