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

description, i18n_source_hash, i18n_provenance, i18n_output_hash, i18n_hash_version
description i18n_source_hash i18n_provenance i18n_output_hash i18n_hash_version
Monorepo-Struktur, App- und Paketarchitektur, Request-Lebenszyklus und Ressourcen-Footprint von SnapOtter. 50e076925c4b human 3700f01eae84 2

Architektur

SnapOtter ist ein Monorepo, das mit pnpm-Workspaces und Turborepo verwaltet wird. Es wird als 3-Container-Docker-Compose-Stack ausgeliefert: das SnapOtter-App-Image, PostgreSQL 17 und Redis 8.

Projektstruktur

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

Pakete

@snapotter/image-engine

Die zentrale Bildverarbeitungsbibliothek, aufgebaut auf Sharp. Sie übernimmt alle Nicht-KI-Operationen: Skalieren, Zuschneiden, Drehen, Spiegeln, Konvertieren, Komprimieren, Metadaten entfernen und Farbanpassungen (Helligkeit, Kontrast, Sättigung, Graustufen, Sepia, Invertieren, Farbkanäle).

Dieses Paket hat keine Netzwerkabhängigkeiten und läuft vollständig im Prozess.

@snapotter/ai

Eine Brückenschicht, die native und Python ML-Laufzeiten aufruft. Die meisten Python-Tools verwenden ein persistentes dispatcher, das umfangreiche Bibliotheken (PIL, NumPy, MediaPipe, rembg) vorimportiert, sodass nachfolgende Aufrufe den Importaufwand überspringen. OCR ist von dieser veränderlichen gemeinsamen Umgebung isoliert: fast ruft natives Tesseract auf, während balanced und best ein dediziertes persistentes JSONL dispatcher verwenden, das an die aktive unveränderliche RapidOCR/ONNX-Generation angeheftet ist. Jede Anfrage enthält einen generation lease. Bei der Aktivierung wird zunächst ein smoke test für einen Kandidaten ausgeführt und dann atomar zu seinem dispatcher gewechselt. Der vorherige dispatcher wird entleert, bevor seine Generierung in die Speicherbereinigung aufgenommen wird.

Modelle werden nicht vorgeladen. Jedes Werkzeug-Skript lädt seine Modellgewichte zur Anfragezeit von der Festplatte und verwirft sie, sobald die Anfrage abgeschlossen ist. Siehe Ressourcen-Footprint für das vollständige Speicherprofil.

Unterstützte Vorgänge: Hintergrundentfernung (rembg/BiRefNet), Hochskalierung (RealESRGAN), Gesichtsunschärfe (MediaPipe), Gesichtsverbesserung (GFPGAN/CodeFormer), Objektlöschung (LaMa ONNX), OCR (Tesseract und RapidOCR mit PP-OCR ONNX-Modellen), Kolorierung (DDColor), Rauschentfernung, Rote-Augen-Entfernung, Fotowiederherstellung, Passfoto Generierung, Transparenzkorrektur (BiRefNet HR-Matting) und inhaltsbezogene Größenänderung (Go Caire Binary).

Python-Skripte leben in packages/ai/python/. Große optionale Modellpakete werden bei Bedarf im persistenten /data/ai-Volume installiert. Accurate OCR verwendet signierte, plattformspezifische Artefakte; Für die integrierte Tesseract-Stufe ist kein Download des Modellpakets erforderlich.

@snapotter/shared

Gemeinsam genutzte TypeScript-Typen, Konstanten (wie APP_VERSION und Werkzeugdefinitionen) und i18n-Übersetzungsstrings, die sowohl vom Frontend als auch vom Backend verwendet werden.

Anwendungen

API (apps/api)

Ein Fastify-v5-Server, der 243 Werkzeug-Routen über fünf Modalitäten (image, video, audio, PDF, file) bereitstellt und Folgendes übernimmt:

  • Datei-Uploads, Verwaltung des temporären Arbeitsbereichs und persistenter Dateispeicher
  • Benutzer-Dateibibliothek (user_files-Tabelle): Ein gespeicherter Edit wird standardmäßig als eigenständige neue Datei abgelegt, oder als übergeordnet verknüpfte Version, wenn du das Original überschreibst. Sie erfasst, welche Werkzeuge angewendet wurden (toolChain), und erhält ein automatisch generiertes Thumbnail für die Files-Seite
  • Werkzeugausführung (leitet jede Werkzeuganfrage an die Image-Engine oder die KI-Brücke weiter)
  • Pipeline-Orchestrierung (das sequenzielle Verketten mehrerer Werkzeuge)
  • Stapelverarbeitung mit Nebenläufigkeitssteuerung über BullMQ-Job-Warteschlangen (Pools: image, media, ai, docs, system)
  • Benutzerauthentifizierung, RBAC (admin-/user-Rollen mit einem vollständigen Berechtigungssatz), API-Schlüsselverwaltung und Ratenbegrenzung
  • Teamverwaltung - Admin-only-CRUD; Benutzer werden über das Feld team in ihrem Profil einem Team zugewiesen
  • Laufzeiteinstellungen - ein Schlüssel-Wert-Speicher in der settings-Tabelle, der disabledTools, enableExperimentalTools, loginAttemptLimit und andere betriebliche Stellschrauben ohne erneutes Deployment steuert
  • Benutzerdefiniertes Branding und Laufzeiteinstellungen über datenbankgestützte Settings
  • Scalar-/OpenAPI-Dokumentation unter /api/docs
  • Auslieferung des gebauten Frontends als SPA in der Produktion

Wichtige Abhängigkeiten: Fastify, Drizzle ORM (pg-core, node-postgres), Sharp, BullMQ, ioredis, Zod für die Validierung.

Der Server behandelt das kontrollierte Herunterfahren bei SIGTERM/SIGINT: Er lässt HTTP-Verbindungen auslaufen, stoppt die BullMQ-Worker, fährt den Python-Dispatcher herunter und schließt die Datenbankverbindung.

Web (apps/web)

Eine React-19-Single-Page-App, gebaut mit Vite. Nutzt Zustand für die Zustandsverwaltung, Tailwind CSS v4 für das Styling und Lucide für Icons. Kommuniziert mit der API über REST und SSE (für die Fortschrittsverfolgung).

Zu den Seiten gehören ein Werkzeug-Arbeitsbereich, eine Files-Seite zur Verwaltung persistenter Uploads und Ergebnisse, ein Automatisierungs-/Pipeline-Builder und ein Admin-Einstellungspanel.

Das gebaute Frontend wird in der Produktion vom Fastify-Backend ausgeliefert, sodass es im Docker-Container keinen separaten Webserver gibt.

Docs (apps/docs)

Diese VitePress-Site. Wird bei jedem Push auf main automatisch auf Cloudflare Pages bereitgestellt.

Wie eine Anfrage abläuft

  1. Der Benutzer wählt in der Web-UI ein Werkzeug aus und lädt eine Datei hoch.
  2. Das Frontend sendet einen Multipart-POST an /api/v1/tools/:section/:toolId mit der Datei und den Einstellungen.
  3. Die API-Route validiert die Eingabe mit Zod und stellt dann die Verarbeitung zu.
  4. Bei Standardwerkzeugen wird der Job in den passenden BullMQ-Pool eingereiht (image, media oder docs je nach Modalität). Der In-Prozess-BullMQ-Worker richtet das Bild anhand der EXIF-Metadaten automatisch aus, führt die Prozessfunktion des Werkzeugs aus und gibt das Ergebnis zurück.
  5. Bei den meisten KI-Tools sendet die TypeScript-Brücke eine Anfrage an den persistenten Python dispatcher. Schnelles OCR ruft stattdessen Tesseract auf, und genaues OCR startet die angeheftete ausführbare Datei aus der aktiven unveränderlichen OCR-Generation. Die angeforderte OCR-Stufe ist beim Eingang festgelegt und wird während der Ausführung nie stillschweigend geändert.
  6. Der Job-Fortschritt wird in der jobs-Tabelle in PostgreSQL persistiert, sodass der Zustand Container-Neustarts überdauert. Echtzeit-Updates werden über SSE unter /api/v1/jobs/:jobId/progress geliefert.
  7. Die API gibt ein jobId und ein downloadUrl zurück. Der Benutzer lädt die verarbeitete Datei von /api/v1/download/:jobId/:filename herunter.

Bei Pipelines führt die API die Ausgabe jedes Schritts als Eingabe an den nächsten weiter und führt sie sequenziell aus.

Bei der Stapelverarbeitung nutzt die API BullMQ-Flows mit Kind-Jobs pro Schritt und gibt eine ZIP-Datei mit allen verarbeiteten Dateien zurück.

Ressourcen-Footprint

SnapOtter ist auf geringen Speicherverbrauch im Leerlauf ausgelegt. Beim Start wird nichts vorgeladen oder warmgehalten.

Im Leerlauf

Der Node.js-/Fastify-Prozess, PostgreSQL und Redis laufen. Der typische Leerlauf-RAM beträgt ~200-300 MB über alle drei Container hinweg (Node.js-Prozess, Postgres und Redis). Kein Python-Prozess, keine Modellgewichte im Speicher.

Was startet, und wann

Komponente Startet bei Speicher während aktiv
Fastify-Server + Postgres + Redis Containerstart ~200-300 MB gesamt
BullMQ-Worker Containerstart (im Prozess) Ein Worker pro Pool (image, media, ai, docs, system)
Python-Dispatcher Erste KI-Werkzeuganfrage Python-Interpreter + vorab importierte Bibliotheken (PIL, NumPy, MediaPipe, rembg) - keine Modellgewichte
KI-Modellgewichte Während der Anfrage des jeweiligen Werkzeugs Von der Festplatte geladen, nach Abschluss der Anfrage freigegeben

Modellladen

Alle Modellgewichtsdateien (insgesamt mehrere GB) liegen jederzeit auf der Festplatte in /opt/models/. Jedes KI-Werkzeug-Skript lädt nur seine eigenen Modelle für die Dauer einer Anfrage in den Speicher und gibt sie dann frei. Einige Skripte rufen nach der Inferenz explizit del model und torch.cuda.empty_cache() auf, um sicherzustellen, dass der Speicher sofort zurückgegeben wird.

Es gibt keinen Modell-Cache zwischen Anfragen. Führt man dasselbe KI-Werkzeug direkt hintereinander aus, wird das Modell jedes Mal neu geladen. Das hält den Leerlaufspeicher nahe null, auf Kosten einer Modellladeverzögerung bei jeder KI-Anfrage.

Kaltstart bei der ersten KI-Anfrage

Der Python-Dispatcher läuft nicht, wenn der Container startet. Die erste KI-Anfrage löst zwei Dinge parallel aus: Der Dispatcher beginnt im Hintergrund aufzuwärmen, und die Anfrage selbst weicht auf einen einmaligen Python-Subprozess-Start aus. Sobald der Dispatcher bereit signalisiert, nutzen alle nachfolgenden KI-Anfragen ihn direkt und sparen sich die Kosten des Subprozess-Starts.