Files
SnapOtter/apps/docs/it/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

9.9 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
Struttura del monorepo, architettura di app e pacchetti, ciclo di vita di una richiesta e impronta sulle risorse di SnapOtter. 50e076925c4b human c3c0755b6082 2

Architettura

SnapOtter è un monorepo gestito con i workspace pnpm e Turborepo. Viene distribuito come stack Docker Compose a 3 container: l'immagine dell'app SnapOtter, PostgreSQL 17 e Redis 8.

Struttura del progetto

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

Pacchetti

@snapotter/image-engine

La libreria core di elaborazione delle immagini costruita su Sharp. Gestisce tutte le operazioni non-AI: ridimensiona, ritaglia, ruota, capovolgi, converti, comprimi, rimuovi i metadati e regola i colori (luminosità, contrasto, saturazione, scala di grigi, seppia, inversione, canali di colore).

Questo pacchetto non ha dipendenze di rete e gira interamente in-process.

@snapotter/ai

Un livello bridge che chiama runtime nativi e Python ML. La maggior parte degli strumenti Python utilizzano un dispatcher persistente che preimporta librerie pesanti (PIL, NumPy, MediaPipe, rembg) in modo che le chiamate successive saltino il sovraccarico dell'importazione. OCR è isolato da quell'ambiente condiviso mutevole: fast richiama Tesseract nativo, mentre balanced e best utilizzano un JSONL dispatcher persistente dedicato aggiunto alla generazione attiva immutabile RapidOCR/ONNX. Ogni richiesta contiene un generation lease. L'attivazione esegue prima un smoke test su un candidato, quindi passa atomicamente al suo dispatcher. Il precedente dispatcher viene scaricato prima che la sua generazione venga sottoposta a garbage collection.

I modelli non sono precaricati. Ogni script dello strumento carica i pesi del proprio modello dal disco al momento della richiesta e li scarta quando la richiesta termina. Consulta Impronta sulle risorse per il profilo di memoria completo.

Operazioni supportate: rimozione dello sfondo (rembg/BiRefNet), upscaling (RealESRGAN), sfocatura del volto (MediaPipe), miglioramento del volto (GFPGAN/CodeFormer), cancellazione degli oggetti (LaMa ONNX), OCR (Tesseract e RapidOCR con modelli PP-OCR ONNX), colorazione (DDColor), rimozione del rumore, rimozione degli occhi rossi, restauro di foto, foto tessera generazione, correzione della trasparenza (BiRefNet HR-matting) e ridimensionamento in base al contenuto (Go caire binario).

Gli script Python risiedono in packages/ai/python/. I pacchetti di modelli opzionali di grandi dimensioni vengono installati su richiesta nel volume /data/ai persistente. OCR accurato utilizza artefatti firmati specifici della piattaforma; il livello Tesseract integrato non richiede il download del pacchetto di modelli.

@snapotter/shared

Tipi TypeScript condivisi, costanti (come APP_VERSION e le definizioni degli strumenti) e stringhe di traduzione i18n usate sia dal frontend sia dal backend.

Applicazioni

API (apps/api)

Un server Fastify v5 che espone 243 route di strumenti su cinque modalità (immagine, video, audio, PDF, file) e gestisce:

  • Upload di file, gestione dello spazio di lavoro temporaneo e archiviazione persistente dei file
  • Libreria di file utente (tabella user_files): per impostazione predefinita, una modifica salvata viene archiviata come nuovo file indipendente, oppure come versione collegata al genitore quando sovrascrivi l'originale. Registra quali strumenti sono stati applicati (toolChain) e ottiene una miniatura auto-generata per la pagina File
  • Esecuzione degli strumenti (instrada ogni richiesta di strumento all'image engine o all'AI bridge)
  • Orchestrazione delle pipeline (concatenamento sequenziale di più strumenti)
  • Elaborazione in batch con controllo della concorrenza tramite le code di lavori BullMQ (pool: image, media, ai, docs, system)
  • Autenticazione utente, RBAC (ruoli admin/user con un set completo di permessi), gestione delle chiavi API e rate limiting
  • Gestione dei team - CRUD solo per admin; gli utenti vengono assegnati a un team tramite il campo team sul loro profilo
  • Impostazioni di runtime - un archivio chiave-valore nella tabella settings che controlla disabledTools, enableExperimentalTools, loginAttemptLimit e altre manopole operative senza ridistribuire
  • Branding personalizzato e preferenze di runtime tramite impostazioni supportate dal database
  • Documentazione Scalar/OpenAPI su /api/docs
  • Servire il frontend compilato come SPA in produzione

Dipendenze principali: Fastify, Drizzle ORM (pg-core, node-postgres), Sharp, BullMQ, ioredis, Zod per la validazione.

Il server gestisce lo spegnimento controllato su SIGTERM/SIGINT: drena le connessioni HTTP, ferma i worker BullMQ, spegne il dispatcher Python e chiude la connessione al database.

Web (apps/web)

Una single-page app React 19 costruita con Vite. Usa Zustand per la gestione dello stato, Tailwind CSS v4 per lo stile e Lucide per le icone. Comunica con l'API tramite REST e SSE (per il tracciamento dell'avanzamento).

Le pagine includono uno spazio di lavoro per gli strumenti, una pagina File per gestire upload e risultati persistenti, un costruttore di automazione/pipeline e un pannello di impostazioni admin.

Il frontend compilato viene servito dal backend Fastify in produzione, quindi non c'è un server web separato nel container Docker.

Docs (apps/docs)

Questo sito VitePress. Distribuito su Cloudflare Pages automaticamente al push su main.

Come scorre una richiesta

  1. L'utente sceglie uno strumento nell'interfaccia web e carica un file.
  2. Il frontend invia un POST multipart a /api/v1/tools/:section/:toolId con il file e le impostazioni.
  3. La route API valida l'input con Zod, poi avvia l'elaborazione.
  4. Per gli strumenti standard, il lavoro viene accodato al pool BullMQ appropriato (image, media o docs in base alla modalità). Il worker BullMQ in-process orienta automaticamente l'immagine in base ai metadati EXIF, esegue la funzione di elaborazione dello strumento e restituisce il risultato.
  5. Per la maggior parte degli strumenti IA, il bridge TypeScript invia una richiesta al persistente Python dispatcher. OCR veloce richiama invece Tesseract e OCR accurato avvia l'eseguibile bloccato dalla generazione OCR immutabile attiva. Il livello OCR richiesto è fisso in ingresso e non viene mai modificato automaticamente durante l'esecuzione.
  6. L'avanzamento del lavoro viene persistito nella tabella jobs in PostgreSQL così che lo stato sopravviva ai riavvii del container. Gli aggiornamenti in tempo reale vengono consegnati via SSE su /api/v1/jobs/:jobId/progress.
  7. L'API restituisce un jobId e un downloadUrl. L'utente scarica il file elaborato da /api/v1/download/:jobId/:filename.

Per le pipeline, l'API passa l'output di ogni passaggio come input al successivo, eseguendoli in sequenza.

Per l'elaborazione in batch, l'API usa i flow BullMQ con lavori figlio per ogni passaggio e restituisce un file ZIP con tutti i file elaborati.

Impronta sulle risorse

SnapOtter è progettato per un basso utilizzo di memoria a riposo. Nulla viene precaricato o tenuto caldo all'avvio.

A riposo

Il processo Node.js/Fastify, PostgreSQL e Redis sono in esecuzione. La RAM tipica a riposo è di ~200-300 MB tra tutti e tre i container (processo Node.js, Postgres e Redis). Nessun processo Python, nessun peso di modello in memoria.

Cosa si avvia, e quando

Componente Si avvia quando Memoria mentre è attivo
Server Fastify + Postgres + Redis Avvio del container ~200-300 MB in totale
Worker BullMQ Avvio del container (in-process) Un worker per pool (image, media, ai, docs, system)
Dispatcher Python Prima richiesta di uno strumento AI Interprete Python + librerie pre-importate (PIL, NumPy, MediaPipe, rembg) - nessun peso di modello
Pesi dei modelli AI Durante la richiesta dello specifico strumento Caricati dal disco, liberati al termine della richiesta

Caricamento dei modelli

Tutti i file dei pesi dei modelli (per un totale di diversi GB) risiedono sul disco in /opt/models/ in ogni momento. Ogni script dello strumento AI carica in memoria solo il proprio modello o i propri modelli per la durata di una richiesta, poi li rilascia. Alcuni script chiamano esplicitamente del model e torch.cuda.empty_cache() dopo l'inferenza per assicurarsi che la memoria venga restituita immediatamente.

Non esiste una cache dei modelli tra le richieste. Eseguire lo stesso strumento AI in successione ricarica il modello ogni volta. Questo mantiene la memoria a riposo prossima allo zero al costo di un ritardo di caricamento del modello a ogni richiesta AI.

Cold start della prima richiesta AI

Il dispatcher Python non è in esecuzione all'avvio del container. La prima richiesta AI innesca due cose in parallelo: il dispatcher inizia a scaldarsi in background e la richiesta stessa ripiega sull'avvio una tantum di un sottoprocesso Python. Una volta che il dispatcher segnala di essere pronto, tutte le richieste AI successive lo usano direttamente e saltano il costo di avvio del sottoprocesso.