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

12 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
SnapOtter のモノレポ構造、アプリとパッケージのアーキテクチャ、リクエストのライフサイクル、リソースフットプリント。 50e076925c4b human 39bcdb8ad563 2

アーキテクチャ

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) を事前インポートする永続的な dispatcher を使用するため、後続の呼び出しではインポートのオーバーヘッドがスキップされます。 OCR は、変更可能な共有環境から分離されています。fast はネイティブ Tesseract を呼び出しますが、balanced および best は、アクティブな不変 RapidOCR/ONNX 世代に固定された専用の永続的な JSONL dispatcher を使用します。 各リクエストは generation lease を保持します。 アクティブ化では、まず候補に対して smoke test が実行され、次にその dispatcher にアトミックに切り替わります。 以前の dispatcher は、その生成がガベージ コレクションされる前にドレインされます。

モデルは事前ロードされません。 各ツールスクリプトは、リクエスト時にディスクからモデルの重みを読み込み、リクエストが完了すると破棄します。メモリプロファイルの全容については リソースフットプリント を参照してください。

サポートされている操作: 背景除去 (rembg/BiRefNet)、アップスケーリング (RealESRGAN)、顔ぼかし (MediaPipe)、顔強調 (GFPGAN/CodeFormer)、オブジェクト消去 (LaMa ONNX)、OCR (Tesseract および PP-OCR ONNX モデルの RapidOCR)、カラー化 (DDColor)、ノイズ除去、赤目除去、写真の復元、パスポート写真の生成、透明度の修正 (BiRefNet HR マット化)、およびコンテンツに応じたサイズ変更 (Go caire バイナリ)。

Python スクリプトは packages/ai/python/ に存在します。大規模なオプションのモデル パックは、オンデマンドで永続 /data/ai ボリュームにインストールされます。正確な OCR は、署名されたプラットフォーム固有のアーティファクトを使用します。組み込みの Tesseract 層では、モデル パックのダウンロードは必要ありません。

@snapotter/shared

フロントエンドとバックエンドの両方で使用される、共有 TypeScript 型、定数(APP_VERSION やツール定義など)、および i18n 翻訳文字列。

アプリケーション

API (apps/api)

5 つのモダリティ(画像、動画、音声、PDF、ファイル)にまたがる 243 のツールルートを公開する Fastify v5 サーバーで、以下を処理します:

  • ファイルアップロード、一時的なワークスペース管理、永続的なファイルストレージ
  • ユーザーファイルライブラリ(user_files テーブル): 保存された編集は、デフォルトでは独立した新しいファイルとして保存され、元のファイルを上書きした場合は親にリンクされたバージョンとして保存されます。どのツールが適用されたか(toolChain)を記録し、Files ページ用の自動生成サムネイルを取得します
  • ツール実行(各ツールリクエストを画像エンジンまたは AI ブリッジにルーティング)
  • パイプラインのオーケストレーション(複数のツールを順次連結)
  • BullMQ ジョブキュー(プール: image、media、ai、docs、system)による並行制御付きのバッチ処理
  • ユーザー認証、RBAC(フルの権限セットを持つ admin/user ロール)、API キー管理、レート制限
  • チーム管理 - admin 専用の CRUD。ユーザーはプロフィールの team フィールドを介してチームに割り当てられます
  • ランタイム設定 - settings テーブルのキーバリューストアで、再デプロイせずに disabledToolsenableExperimentalToolsloginAttemptLimit などの運用ノブを制御します
  • データベースに保存された設定によるカスタムブランディングとランタイム設定
  • /api/docs での Scalar/OpenAPI ドキュメント
  • 本番環境で、ビルドされたフロントエンドを SPA として配信

主な依存関係: Fastify、Drizzle ORMpg-core、node-postgres)、Sharp、BullMQ、ioredis、検証用の Zod。

サーバーは SIGTERM/SIGINT でのグレースフルシャットダウンを処理します。HTTP 接続をドレインし、BullMQ ワーカーを停止し、Python ディスパッチャーをシャットダウンし、データベース接続を閉じます。

Web (apps/web)

Vite でビルドされた React 19 のシングルページアプリです。状態管理に Zustand、スタイリングに Tailwind CSS v4、アイコンに Lucide を使用します。API とは REST と SSE(進捗トラッキング用)で通信します。

ページには、ツールワークスペース、永続的なアップロードと結果を管理する Files ページ、自動化/パイプラインビルダー、admin 設定パネルが含まれます。

ビルドされたフロントエンドは本番環境で Fastify バックエンドによって配信されるため、Docker コンテナ内に別のウェブサーバーはありません。

Docs (apps/docs)

この VitePress サイトです。main へのプッシュ時に、Cloudflare Pages へ自動的にデプロイされます。

リクエストの流れ

  1. ユーザーが Web UI でツールを選び、ファイルをアップロードします。
  2. フロントエンドが、ファイルと設定を添えて /api/v1/tools/:section/:toolId へマルチパート POST を送信します。
  3. API ルートが Zod で入力を検証し、処理をディスパッチします。
  4. 標準ツールの場合、ジョブは適切な BullMQ プール(モダリティに応じて image、media、または docs)にエンキューされます。プロセス内の BullMQ ワーカーが、EXIF メタデータに基づいて画像を自動回転させ、ツールのプロセス関数を実行して結果を返します。
  5. ほとんどの AI ツールでは、TypeScript ブリッジは永続的な Python dispatcher にリクエストを送信します。 高速 OCR は代わりに Tesseract を呼び出し、正確な OCR はアクティブな不変 OCR 世代から固定された実行可能ファイルを開始します。 要求された OCR 層は入力時に固定され、実行中にサイレントに変更されることはありません。
  6. ジョブの進捗は PostgreSQL の jobs テーブルに永続化されるため、状態はコンテナの再起動を越えて保持されます。リアルタイム更新は /api/v1/jobs/:jobId/progress の SSE で配信されます。
  7. API は jobIddownloadUrl を返します。ユーザーは /api/v1/download/:jobId/:filename から処理済みファイルをダウンロードします。

パイプラインの場合、API は各ステップの出力を次のステップの入力として順次実行します。

バッチ処理の場合、API はステップごとの子ジョブを持つ BullMQ フローを使用し、すべての処理済みファイルを含む ZIP ファイルを返します。

リソースフットプリント

SnapOtter は、アイドル時のメモリ使用量を低く抑えるように設計されています。起動時に事前ロードされたり、ウォームな状態で保持されたりするものはありません。

アイドル時

Node.js/Fastify プロセス、PostgreSQL、Redis が動作しています。典型的なアイドル時の RAM は、3 つのコンテナ(Node.js プロセス、Postgres、Redis)全体で 約 200〜300 MB です。Python プロセスも、メモリ上のモデルの重みもありません。

何が、いつ起動するか

コンポーネント 起動タイミング アクティブ時のメモリ
Fastify サーバー + Postgres + Redis コンテナ起動時 合計 約 200〜300 MB
BullMQ ワーカー コンテナ起動時(プロセス内) プールごとに 1 ワーカー(image、media、ai、docs、system
Python ディスパッチャー 最初の AI ツールリクエスト時 Python インタープリタ + 事前インポートされたライブラリ(PIL、NumPy、MediaPipe、rembg - モデルの重みなし
AI モデルの重み 特定のツールのリクエスト中 ディスクからロードされ、リクエスト完了時に解放される

モデルのロード

すべてのモデル重みファイル(合計で数 GB)は、常に /opt/models/ のディスク上に置かれています。各 AI ツールスクリプトは、リクエストの間だけ自身のモデルのみをメモリにロードし、その後解放します。一部のスクリプトは、メモリを即座に返却するために推論後に明示的に del modeltorch.cuda.empty_cache() を呼び出します。

リクエスト間のモデルキャッシュはありません。同じ AI ツールを連続して実行すると、その都度モデルが再ロードされます。これにより、AI リクエストのたびにモデルロードの遅延が生じる代わりに、アイドル時のメモリをゼロ近くに保ちます。

最初の AI リクエストのコールドスタート

コンテナ起動時、Python ディスパッチャーは動作していません。最初の AI リクエストは、2 つのことを並行して引き起こします。ディスパッチャーがバックグラウンドでウォームアップを開始し、リクエスト自体は一回限りの Python サブプロセス生成にフォールバックします。ディスパッチャーが準備完了を通知すると、以降のすべての AI リクエストはそれを直接使用し、サブプロセス生成のコストをスキップします。