Files
SnapOtter/apps/docs/id/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.3 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
Struktur monorepo, arsitektur aplikasi dan paket, siklus hidup permintaan, dan jejak sumber daya SnapOtter. 50e076925c4b human d266f832d3f1 2

Arsitektur

SnapOtter adalah monorepo yang dikelola dengan workspace pnpm dan Turborepo. Ia di-deploy sebagai stack Docker Compose 3 kontainer: image aplikasi SnapOtter, PostgreSQL 17, dan Redis 8.

Struktur proyek

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

Paket

@snapotter/image-engine

Pustaka pemrosesan gambar inti yang dibangun di atas Sharp. Ia menangani semua operasi non-AI: resize, crop, rotate, flip, convert, compress, strip metadata, dan penyesuaian warna (kecerahan, kontras, saturasi, grayscale, sepia, invert, saluran warna).

Paket ini tidak memiliki dependensi jaringan dan berjalan sepenuhnya dalam proses.

@snapotter/ai

Lapisan jembatan yang memanggil runtime asli dan Python ML. Sebagian besar alat Python menggunakan dispatcher persisten yang melakukan pra-impor pustaka berat (PIL, NumPy, MediaPipe, rembg) sehingga panggilan berikutnya melewati overhead impor. OCR diisolasi dari lingkungan bersama yang dapat diubah: fast memanggil Tesseract asli, sementara balanced dan best menggunakan JSONL dispatcher persisten khusus yang disematkan pada generasi RapidOCR/ONNX aktif yang tidak dapat diubah. Setiap permintaan memiliki generation lease. Aktivasi pertama-tama menjalankan smoke test pada kandidat, kemudian beralih secara atom ke dispatcher-nya. Saluran air dispatcher sebelumnya sebelum pembangkitannya dikumpulkan sampahnya.

Model tidak dimuat terlebih dahulu. Setiap skrip tool memuat bobot modelnya dari disk pada waktu permintaan dan membuangnya saat permintaan selesai. Lihat Jejak sumber daya untuk profil memori lengkap.

Operasi yang didukung: penghapusan latar belakang (rembg/BiRefNet), peningkatan (RealESRGAN), keburaman wajah (MediaPipe), penyempurnaan wajah (GFPGAN/CodeFormer), penghapusan objek (LaMa ONNX), OCR (Tesseract dan RapidOCR dengan model PP-OCR ONNX), pewarnaan (DDColor), penghilangan noise, penghilangan mata merah, restorasi foto, pembuatan foto paspor, perbaikan transparansi (BiRefNet HR-matting), dan pengubahan ukuran berdasarkan konten (Go caire biner).

Skrip Python ada di packages/ai/python/. Paket model opsional berukuran besar dipasang sesuai permintaan ke dalam volume /data/ai yang persisten. OCR yang akurat menggunakan artefak khusus platform yang ditandatangani; tingkat Tesseract bawaan tidak memerlukan pengunduhan paket model.

@snapotter/shared

Tipe TypeScript bersama, konstanta (seperti APP_VERSION dan definisi tool), dan string terjemahan i18n yang digunakan oleh frontend dan backend.

Aplikasi

API (apps/api)

Server Fastify v5 yang mengekspos 243 route tool di lima modalitas (image, video, audio, PDF, file) yang menangani:

  • Unggahan file, manajemen workspace sementara, dan penyimpanan file persisten
  • Pustaka file pengguna (tabel user_files): secara default, sebuah editan yang disimpan disimpan sebagai file baru yang independen, atau sebagai versi yang tertaut ke induk ketika Anda menimpa file asli. Ia mencatat tool mana yang diterapkan (toolChain) dan mendapatkan thumbnail yang dibuat otomatis untuk halaman Files
  • Eksekusi tool (mengarahkan setiap permintaan tool ke image engine atau AI bridge)
  • Orkestrasi pipeline (merangkai beberapa tool secara berurutan)
  • Pemrosesan batch dengan kontrol konkurensi melalui antrean job BullMQ (pool: image, media, ai, docs, system)
  • Autentikasi pengguna, RBAC (peran admin/user dengan set izin lengkap), manajemen kunci API, dan pembatasan laju
  • Manajemen Teams - CRUD hanya-admin; pengguna ditugaskan ke sebuah tim melalui field team di profil mereka
  • Pengaturan runtime - penyimpanan key-value di tabel settings yang mengontrol disabledTools, enableExperimentalTools, loginAttemptLimit, dan tombol operasional lainnya tanpa deploy ulang
  • Branding kustom dan preferensi runtime melalui pengaturan yang didukung basis data
  • Dokumentasi Scalar/OpenAPI di /api/docs
  • Menyajikan frontend yang telah dibangun sebagai SPA dalam produksi

Dependensi utama: Fastify, Drizzle ORM (pg-core, node-postgres), Sharp, BullMQ, ioredis, Zod untuk validasi.

Server menangani penghentian yang mulus pada SIGTERM/SIGINT: ia menguras koneksi HTTP, menghentikan worker BullMQ, mematikan dispatcher Python, dan menutup koneksi basis data.

Web (apps/web)

Aplikasi single-page React 19 yang dibangun dengan Vite. Menggunakan Zustand untuk manajemen state, Tailwind CSS v4 untuk penataan, dan Lucide untuk ikon. Berkomunikasi dengan API melalui REST dan SSE (untuk pelacakan progres).

Halaman mencakup workspace tool, halaman Files untuk mengelola unggahan dan hasil persisten, pembuat automasi/pipeline, dan panel pengaturan admin.

Frontend yang telah dibangun disajikan oleh backend Fastify dalam produksi, jadi tidak ada server web terpisah di kontainer Docker.

Docs (apps/docs)

Situs VitePress ini. Di-deploy ke Cloudflare Pages secara otomatis saat push ke main.

Bagaimana sebuah permintaan mengalir

  1. Pengguna memilih sebuah tool di UI web dan mengunggah file.
  2. Frontend mengirim POST multipart ke /api/v1/tools/:section/:toolId dengan file dan pengaturan.
  3. Route API memvalidasi input dengan Zod, lalu mengirim pemrosesan.
  4. Untuk tool standar, job dimasukkan ke antrean ke pool BullMQ yang sesuai (image, media, atau docs berdasarkan modalitas). Worker BullMQ dalam proses secara otomatis mengorientasikan gambar berdasarkan metadata EXIF, menjalankan fungsi proses tool, dan mengembalikan hasilnya.
  5. Untuk sebagian besar alat AI, jembatan TypeScript mengirimkan permintaan ke Python dispatcher yang persisten. OCR yang cepat malah memanggil Tesseract, dan OCR yang akurat memulai eksekusi yang disematkan dari generasi OCR aktif yang tidak dapat diubah. Tingkat OCR yang diminta ditetapkan saat masuk dan tidak pernah diubah secara diam-diam selama eksekusi.
  6. Progres job dipertahankan ke tabel jobs di PostgreSQL sehingga state bertahan saat kontainer dimulai ulang. Pembaruan waktu nyata dikirimkan melalui SSE di /api/v1/jobs/:jobId/progress.
  7. API mengembalikan jobId dan downloadUrl. Pengguna mengunduh file yang telah diproses dari /api/v1/download/:jobId/:filename.

Untuk pipeline, API memberi output setiap langkah sebagai input ke langkah berikutnya, menjalankannya secara berurutan.

Untuk pemrosesan batch, API menggunakan flow BullMQ dengan child job per langkah dan mengembalikan file ZIP berisi semua file yang diproses.

Jejak sumber daya

SnapOtter dirancang untuk penggunaan memori idle yang rendah. Tidak ada yang dimuat terlebih dahulu atau dijaga tetap hangat saat startup.

Saat idle

Proses Node.js/Fastify, PostgreSQL, dan Redis berjalan. RAM idle tipikal adalah ~200-300 MB di ketiga kontainer (proses Node.js, Postgres, dan Redis). Tidak ada proses Python, tidak ada bobot model di memori.

Apa yang mulai, dan kapan

Komponen Mulai saat Memori saat aktif
Server Fastify + Postgres + Redis Kontainer mulai ~200-300 MB total
Worker BullMQ Kontainer mulai (dalam proses) Satu worker per pool (image, media, ai, docs, system)
Dispatcher Python Permintaan tool AI pertama Interpreter Python + pustaka yang diimpor terlebih dahulu (PIL, NumPy, MediaPipe, rembg) - tanpa bobot model
Bobot model AI Selama permintaan tool spesifik Dimuat dari disk, dibebaskan saat permintaan selesai

Pemuatan model

Semua file bobot model (berjumlah beberapa GB) berada di disk di /opt/models/ setiap saat. Setiap skrip tool AI hanya memuat model miliknya sendiri ke memori selama durasi permintaan, lalu melepaskannya. Beberapa skrip secara eksplisit memanggil del model dan torch.cuda.empty_cache() setelah inferensi untuk memastikan memori segera dikembalikan.

Tidak ada cache model antar permintaan. Menjalankan tool AI yang sama berturut-turut memuat ulang model setiap kali. Ini menjaga memori idle mendekati nol dengan biaya penundaan pemuatan model pada setiap permintaan AI.

Cold start permintaan AI pertama

Dispatcher Python tidak berjalan saat kontainer dimulai. Permintaan AI pertama memicu dua hal secara paralel: dispatcher mulai memanas di latar belakang, dan permintaan itu sendiri mundur ke pemunculan subproses Python sekali pakai. Setelah dispatcher menandakan siap, semua permintaan AI berikutnya menggunakannya secara langsung dan melewati biaya pemunculan subproses.