This page was machine-translated. Spotted a mistake?Help improve it.
Skip to content

Архитектура

SnapOtter представляет собой монорепозиторий, управляемый с помощью рабочих пространств pnpm и Turborepo. Он развёртывается как стек из 3 контейнеров Docker Compose: образ приложения SnapOtter, PostgreSQL 17 и Redis 8.

Структура проекта

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

Пакеты

@snapotter/image-engine

Основная библиотека обработки изображений, построенная на Sharp. Она выполняет все операции без AI: изменение размера, обрезку, поворот, отражение, преобразование, сжатие, удаление метаданных и цветовые корректировки (яркость, контраст, насыщенность, оттенки серого, сепия, инверсия, цветовые каналы).

У этого пакета нет сетевых зависимостей, и он работает полностью в рамках процесса.

@snapotter/ai

Прослойка, которая вызывает Python-скрипты для ML-операций. При первом использовании прослойка запускает постоянный процесс-диспетчер Python, который предварительно импортирует тяжёлые библиотеки (PIL, NumPy, MediaPipe, rembg), чтобы последующие вызовы AI не несли накладных расходов на импорт. Если диспетчер ещё не готов, прослойка возвращается к порождению нового подпроцесса Python на каждый запрос.

Модели не загружаются заранее. Каждый скрипт инструмента загружает веса своей модели с диска в момент запроса и освобождает их после завершения запроса. Полный профиль памяти см. в разделе Потребление ресурсов.

Поддерживаемые операции: удаление фона (rembg/BiRefNet), апскейлинг (RealESRGAN), размытие лиц (MediaPipe), улучшение лиц (GFPGAN/CodeFormer), стирание объектов (LaMa ONNX), OCR (PaddleOCR/Tesseract), колоризация (DDColor), удаление шума, удаление эффекта красных глаз, реставрация фотографий, создание фото на паспорт, исправление прозрачности (HR-маттинг BiRefNet) и изменение размера с учётом содержимого (Go-бинарник caire).

Python-скрипты находятся в packages/ai/python/. Образ Docker предварительно загружает все веса моделей во время сборки, поэтому контейнер полностью работает офлайн.

@snapotter/shared

Общие типы TypeScript, константы (такие как APP_VERSION и определения инструментов) и строки перевода i18n, используемые как фронтендом, так и бэкендом.

Приложения

API (apps/api)

Сервер Fastify v5, предоставляющий 241 маршрут инструментов в пяти модальностях (image, video, audio, PDF, file), который обрабатывает:

  • Загрузку файлов, управление временным рабочим пространством и постоянное хранение файлов
  • Библиотеку файлов пользователя с цепочками версий (таблица user_files), каждый обработанный результат ссылается на исходный файл и записывает, какой инструмент был применён, с автоматически генерируемыми миниатюрами для страницы Files
  • Выполнение инструментов (направляет каждый запрос инструмента в движок изображений или прослойку AI)
  • Оркестрацию конвейеров (последовательное объединение нескольких инструментов в цепочку)
  • Пакетную обработку с управлением параллелизмом через очереди задач BullMQ (пулы: image, media, ai, docs, system)
  • Аутентификацию пользователей, RBAC (роли admin/user с полным набором прав), управление API-ключами и ограничение частоты запросов
  • Управление командами, CRUD только для администратора; пользователи назначаются в команду через поле team в их профиле
  • Настройки времени выполнения, хранилище «ключ-значение» в таблице settings, которое управляет disabledTools, enableExperimentalTools, loginAttemptLimit и другими эксплуатационными параметрами без повторного развёртывания
  • Пользовательский брендинг и предпочтения времени выполнения через настройки, хранящиеся в базе данных
  • Документацию Scalar/OpenAPI по адресу /api/docs
  • Отдачу собранного фронтенда как SPA в продакшене

Ключевые зависимости: Fastify, Drizzle ORM (pg-core, node-postgres), Sharp, BullMQ, ioredis, Zod для валидации.

Сервер обрабатывает корректное завершение работы по SIGTERM/SIGINT: он сливает HTTP-соединения, останавливает воркеры BullMQ, завершает диспетчер Python и закрывает соединение с базой данных.

Web (apps/web)

Одностраничное приложение на React 19, собранное с помощью Vite. Использует Zustand для управления состоянием, Tailwind CSS v4 для стилизации и Lucide для значков. Обменивается данными с API по REST и SSE (для отслеживания прогресса).

В число страниц входят: рабочее пространство инструментов, страница Files для управления постоянными загрузками и результатами, конструктор автоматизации/конвейеров и панель настроек администратора.

Собранный фронтенд отдаётся бэкендом Fastify в продакшене, поэтому в контейнере Docker нет отдельного веб-сервера.

Docs (apps/docs)

Этот сайт на VitePress. Развёртывается на Cloudflare Pages автоматически при пуше в main.

Как проходит запрос

  1. Пользователь выбирает инструмент в веб-интерфейсе и загружает файл.
  2. Фронтенд отправляет multipart-POST на /api/v1/tools/:section/:toolId с файлом и настройками.
  3. Маршрут API проверяет входные данные с помощью Zod, затем запускает обработку.
  4. Для стандартных инструментов задача ставится в соответствующий пул BullMQ (image, media или docs в зависимости от модальности). Внутрипроцессный воркер BullMQ автоматически ориентирует изображение на основе метаданных EXIF, выполняет функцию обработки инструмента и возвращает результат.
  5. Для AI-инструментов прослойка TypeScript отправляет запрос постоянному диспетчеру Python (или в качестве запасного варианта порождает новый подпроцесс), ждёт его завершения и читает выходной файл.
  6. Прогресс задачи сохраняется в таблицу jobs в PostgreSQL, поэтому состояние переживает перезапуски контейнера. Обновления в реальном времени доставляются через SSE по адресу /api/v1/jobs/:jobId/progress.
  7. API возвращает jobId и downloadUrl. Пользователь скачивает обработанный файл из /api/v1/download/:jobId/:filename.

Для конвейеров API подаёт вывод каждого шага как входные данные для следующего, выполняя их последовательно.

Для пакетной обработки API использует потоки BullMQ с дочерними задачами на каждый шаг и возвращает ZIP-файл со всеми обработанными файлами.

Потребление ресурсов

SnapOtter спроектирован для низкого потребления памяти в простое. Ничего не загружается заранее и не держится «прогретым» при запуске.

В простое

Процесс Node.js/Fastify, PostgreSQL и Redis запущены. Типичное потребление ОЗУ в простое составляет ~200-300 МБ по всем трём контейнерам (процесс Node.js, Postgres и Redis). Нет процесса Python, нет весов моделей в памяти.

Что запускается и когда

КомпонентЗапускается когдаПамять во время активности
Сервер Fastify + Postgres + RedisЗапуск контейнера~200-300 МБ суммарно
Воркеры BullMQЗапуск контейнера (внутрипроцессно)Один воркер на пул (image, media, ai, docs, system)
Диспетчер PythonПервый запрос AI-инструментаИнтерпретатор Python + предварительно импортированные библиотеки (PIL, NumPy, MediaPipe, rembg), без весов моделей
Веса AI-моделейВо время запроса конкретного инструментаЗагружаются с диска, освобождаются по завершении запроса

Загрузка моделей

Все файлы весов моделей (суммарно несколько ГБ) постоянно находятся на диске в /opt/models/. Каждый скрипт AI-инструмента загружает в память только свою(-и) модель(-и) на время запроса, затем освобождает их. Некоторые скрипты явно вызывают del model и torch.cuda.empty_cache() после инференса, чтобы память возвращалась немедленно.

Между запросами кэша моделей нет. Запуск одного и того же AI-инструмента подряд перезагружает модель каждый раз. Это удерживает память в простое около нуля ценой задержки на загрузку модели при каждом AI-запросе.

Холодный старт первого AI-запроса

Диспетчер Python не запущен при старте контейнера. Первый AI-запрос запускает две вещи параллельно: диспетчер начинает прогреваться в фоне, а сам запрос откатывается к разовому порождению подпроцесса Python. Как только диспетчер сигнализирует о готовности, все последующие AI-запросы используют его напрямую и обходят затраты на порождение подпроцесса.