ARCHITECTURE

アーキテクチャ

中核となるコンテナ 3 つに mDNS サービスを加えた構成で、独立したメッセージブローカーもクラウドサービスも必要ありません。アップロードまたは NAS のフォルダーから取り込んだ写真は、メタデータ、サムネイル、セマンティックベクトル、顔、文字の順に処理され、自然言語・人物・場所で検索できるライブラリになります。

3 + 1コンテナ

Go バックエンド、AI サービス、PostgreSQL、そして LAN ドメインを告知する mDNS

5種類のジョブ

metadata、thumbnail、clip、face、ocr

512–1024次元ベクトル

Chinese-CLIP の画像・テキストベクトル(モデルティアにより 512・768・1024 次元)、ArcFace の顔特徴量は 512 次元

HNSWベクトル索引

pgvector のコサイン距離索引。検索と顔の照合はデータベース内で行います

SYSTEM

システム構成

ブラウザが接続するのは Go バックエンドだけ。バックエンドがアップロード、ジョブスケジューリング、サムネイル、権限管理を担い、プレビュー画像を AI サービスに渡して推論させ、結果をすべて PostgreSQL に書き込みます。AI サービスはステートレスで、データベースにアクセスせずファイルも保存しないため、より高性能なマシンに分離して配置できます。

クライアントPC のブラウザWeb アプリは Go バイナリに内蔵スマホのブラウザiPhone · Androidwanmory.localLAN ドメイン、IP を覚える必要なしHTTP :8080DOCKER COMPOSEGo バックエンド · serverAPI、アップロード、ジョブキュー、サムネイル、権限、静的 WebREST APItus 再開可能アップジョブキュー Workerアカウント · ログlibvips サムネイルExifTool メタデータFFmpeg 動画逆ジオコーディングアルバム · スマート連写 · 類似AI ピック · 今日フォルダー 15 分毎mdnswanmory.local を告知AI サービス · mlFastAPI + ONNX Runtime / PyTorch、ステートレスSCRFD 顔検出ArcFace 顔特徴量Chinese-CLIPPP-OCR 文字認識PostgreSQL 17 + pgvector業務データ、ジョブキュー、ベクトルを 1 つの DB に。Redis 不要assetsfaces · peoplejobs キューsettingsHNSW 索引SQL/predict · 1440px プレビューストレージ · 拡張/dataアップロードした原本、サムネイル、1440px プレビュー/photos/*(読み取り専用 :ro 可)NAS 上の既存の写真フォルダー、複数マウント可./modelsモデルファイル、Web の設定からダウンロードリモート AI サービス(任意)Mac(CoreML / MPS)· NVIDIA CUDAトークン認証、1440px プレビューのみ受信、原本には触れない読み書きスキャン · 読み取り専用読み込みHTTP + トークン
バックエンドは Go のバイナリ 1 つだけ

Web アプリはビルドしてバイナリに埋め込まれているため、導入に Node は不要。起動時にデータベースのマイグレーションが自動で走り、アップグレードはイメージを入れ替えるだけです。

AI サービスは別マシンにも配置可能

内蔵の AI サービスは Docker の内部ネットワークで動き、CPU で推論します。Web の設定でリモートのアドレスを入力すれば、家の GPU 搭載 PC や Mac に切り替えられます。

すべてのデータを 1 つのデータベースに

写真の情報、人物、設定、ジョブキュー、ベクトルはすべて PostgreSQL に保存されるため、業務データとジョブの状態はまとめてバックアップできます。写真ファイルは別途バックアップが必要です。

PIPELINE

写真処理パイプライン

取り込んだ写真は、まずメタデータの抽出とサムネイルの生成を行い、続いてセマンティックベクトルの計算、顔と文字の認識を並列で進めます。各ステップは個別にキューに入るため、再起動しても AI サービスが落ちてもジョブは失われず、復旧後に処理が再開されます。

01ingest
取り込み
  • Web アップロードは tus で、切断後も再開可能
  • /photos フォルダーを 15 分ごとにスキャン
  • SHA-1 チェックサムを計算して assets に記録
バックエンド
02metadata
メタデータ
  • ExifTool でサイズと撮影日時を取得
  • GPS、カメラ、レンズ、絞りとシャッター
  • タイムゾーンのない日時は設定値で解釈
バックエンド
03thumbnail
サムネイル
  • libvips でサムネイルを生成
  • さらに 1440px のプレビューを生成
  • 動画は FFmpeg でフレーム抽出・変換
バックエンド
04clip
ベクトル
  • Chinese-CLIP で 1024 次元ベクトルを計算
  • 自然言語検索、自動分類
  • 美的スコア、類似写真
AI · 並列
05face
顔
  • SCRFD で顔を検出し 5 点で位置合わせ
  • ArcFace で 512 次元の特徴量を計算
  • 人物へ逐次グループ化
AI · 並列
06ocr
文字
  • PP-OCR で写真内の文字を認識
  • 文字で写真を検索
  • 証明書・書類・スクショを判定
AI · 並列
SCHEDULING

バックグラウンドジョブのスケジューリング

独立したメッセージブローカーはありません。ジョブキューは PostgreSQL の jobs テーブルで、写真データと同じトランザクションで書き込まれるため、写真が保存されれば必ずジョブもキューに入ります。ワーカーは SKIP LOCKED でジョブを取得し、リース機構で停滞を防ぎ、失敗時は二乗バックオフで再試行します。

pending待機中runningワーカーが取得done完了failed試行回数切れSKIP LOCKED で取得成功再試行回数を使い切る失敗:n² × 30 秒後に再試行AI に接続できない:15 秒後に再試行、失敗に数えないリース 10 分:期限内に結果が返らなければ 30 秒ごとに回収し pending に戻す
jobs/worker.go · claim
-- ジョブを 1 件取得する(server/internal/jobs/worker.go)
UPDATE jobs SET status = 'running', attempts = attempts + 1,
       locked_by = $1, locked_until = now() + $2::interval
WHERE id = (
  SELECT id FROM jobs
  WHERE status = 'pending' AND run_after <= now() AND type = ANY($3)
  ORDER BY priority DESC, run_after
  FOR UPDATE SKIP LOCKED   -- 複数のワーカーが同時に取得しても同じジョブは取らない
  LIMIT 1
)
RETURNING id, type, asset_id, payload, attempts, max_attempts;
スケジューリングのルール
  • 同じ写真の同じ種類のジョブが待機中または実行中なら、重複してキューに入れません。
  • キューが空のときは 1 秒ごとに確認し、priority の高い順、予定時刻の早い順に取得します。
  • 各ジョブのリース(タイムアウトも兼ねる)は 10 分。プロセスの再起動時には、自分が抱えていた未完了ジョブをキューに戻します。
譲り合いと流量制御
  • 誰かが写真をアップロードしている間は AI ジョブの取得を止め、帯域と CPU をアップロードに優先して回します。
  • AI サービスに接続できないときは AI ワーカー全体が停止して待ちます。ジョブを 1 件ずつ取り出しては失敗させ、再試行回数を無駄に消費することはありません。
  • 「自動」ティアでは、まず AI サーバーのハードウェアを検出して適切なモデルを決めてからジョブを取得します。Raspberry Pi などで利用可能なメモリを超えるモデルを読み込むのを避けるのに役立ちます。
AI SCHEDULING

AI スケジューリング

AI サービスの役割は、プレビュー画像を受け取って顔、ベクトル、文字を返すことだけ。サービスのアドレス、モデル、並列数はバックエンドが決め、AI サービスはハードウェアに応じて CUDA、CoreML / MPS、OpenVINO、CPU から選びます。

AI ジョブキューclip · face · ocr並列数は設定どおり、流量制御も調整可Go バックエンド · AI クライアントサービス・ティアを選択、model_ids を照合· 15 秒ごとに /health を確認· 接続不可 → ジョブはキューで待機· ティア自動 → ハードウェアで推奨取得内蔵 AI サービスDocker 内部ネットワーク · CPUリモート AI サービス家の Mac · GPU 搭載 PCリモートワーカーノード自らジョブを取得(計画中)どちらか一方CPUONNX RuntimeCoreML / MPS · CUDA使える高速化を自動選択OpenVINOIntel 内蔵 GPU
3 段階のモデル構成

「自動」を選ぶと AI サーバーのハードウェアに応じて推奨します。使える GPU があれば高精度、CPU のみの場合はメモリ 6 GB 未満なら軽量、6 GB 以上ならバランス。

ティア適したハードウェア顔セマンティック検索
quality · 高精度Apple シリコン Mac、NVIDIA GPUSCRFD-10G + ArcFace R100(antelopev2)Chinese-CLIP ViT-H/14 · 1024 次元
balanced · バランスIntel i5 クラスの NASSCRFD-10G + ArcFace R100(antelopev2)Chinese-CLIP ViT-L/14 · 768 次元
lite · 軽量N100、Raspberry Pi 5SCRFD + ArcFace R50(buffalo_l)Chinese-CLIP ViT-B/16 · 512 次元
AI サービス API · /predict
POST /predict   (multipart: image + tasks)
Authorization: Bearer <トークン>

tasks = {
  "face": {"model": "antelopev2", "min_score": 0.5},
  "clip": {"model": "chinese-clip-vit-h-14"},
  "ocr":  {}
}

→ {
  "face": [{"box": [x, y, w, h], "score": 0.93,
            "embedding": [512 個の数値], "yaw": 0.12, "blur": 340}],
  "clip": {"embedding": [1024 個の数値]},
  "ocr":  {"text": "...", "lines": [...]},
  "model_ids": {"face": "antelopev2", "clip": "chinese-clip-vit-h-14"}
}
ALGORITHMS

コアアルゴリズム

検索、人物、連写、AI ピックアップの裏側で使われているモデル、アルゴリズム、しきい値。AI モデルに頼るものもあれば、ベクトル計算とルールで実現しているものもあります。

自然言語検索

Chinese-CLIP は写真と文章を同じベクトル空間に置きます。写真の取り込み時に画像ベクトルを計算しておき、検索時には「赤ちゃんの 3 歳の誕生日」のような文をテキストベクトルにして、pgvector の HNSW 索引でコサイン距離が最も近い写真を探します。

sim(q, p) = q · p / (‖q‖ ‖p‖)
  • 3 系統の結果を統合:写真内にその文字が認識されたものを最優先し、次にその地名で撮られたもの、最後に意味が最も近いものを並べます。
  • 明らかに一致するものだけを返します:最も近い 1 枚とほぼ同じくらい近く、かつ一般的な写真より明確に近いことが条件で、関連の薄い写真まで返さず弱い一致を除外するのに役立ちます。
  • 検索語がちょうどカテゴリ名(風景、グルメ、スクリーンショット……)なら、そのカテゴリをそのまま返します。
顔のグループ化

SCRFD で顔と 5 つの特徴点を見つけ、特徴点に合わせて 112×112 に切り出し、ArcFace で 512 次元の特徴量を算出します(正規化後の内積がそのままコサイン類似度)。写真 1 枚の処理が終わるたびに逐次グループ化するため、全体の計算完了を待つ必要はありません。

d(a, b) = 1 − a · b  良い顔 d < 0.55 · ぼやけ・横顔 d < 0.45
  • 十分に鮮明で正面向き、大きさも十分な「良い顔」は、最も似た顔を探します。すでに人物に属している顔があれば多数決でその人物へ。どれも未所属でも似た顔が 3 枚以上あれば新しい人物を作ります。
  • ぼやけた顔、横顔、小さすぎる顔は新しい人物を作らず、特定の人物によく似ている場合だけその人物に加わります。たまたま一度写っただけの通行人は登録されません。
  • 複数のジョブが同時にグループ化するときは同じデータベースのアドバイザリーロックを取るため、一人が二人に分かれることはありません。カバー写真には鮮明(ラプラシアン分散)で正面(yaw)、十分に大きい一人だけの顔を選びます。
連写と類似写真
同一カメラで Δt ≤ 2s または Δt ≤ 60s かつ sim ≥ 0.93
  • 同じ場面を続けて撮った写真の CLIP 類似度は通常 0.94〜0.98、場面やポーズが変わると 0.7〜0.89 程度です。0.93 を類似度のしきい値にすることで、連写の可能性が高い写真を見つけやすくなります。
  • おすすめとして残す 1 枚:顔があれば顔が鮮明で目を開けてカメラを見ているもの。顔がなければファイルサイズで判断します(同じカメラ・同じ設定なら、ぶれた写真は細部が少なく圧縮後のサイズが小さくなります)。
  • 同じファイルが複数保存されている場合は「重複」に分けるので、選ぶ必要はありません。「残す」を押すと残りは「最近削除」に移り、完全に削除するまでは復元できます。
AI ピックアップの美的スコア

別のモデルは動かさず、CLIP ベクトルを再利用します。「写真作品」を表す一群の説明文と「普段のスナップ」を表す一群の説明文からそれぞれ平均ベクトルを求め、差を取ると「見栄えが良い」方向が得られます。写真のベクトルをこの方向に射影した値がスコアです。

score(p) = p · (μ作品 − μスナップ)
  • 順位付けにだけ使い、絶対的な点数としては扱いません。ホームのピックアップ・カルーセルはこのスコアで写真を選びます。
  • 別の説明文の組で「撮り損ね」(ぶれ、白飛びなど)を判定し、見栄えの良し悪しとは分けて計算します。
TECH STACK

使用している技術

いずれも成熟したオープンソースのコンポーネントで、独自サービスはありません。どの部品にも代替があります。

バックエンド
  • Go
    API、ジョブキュー、サムネイルのスケジューリング
  • libvips
    サムネイルとプレビュー
  • FFmpeg
    動画のフレーム抽出・変換
  • ExifTool
    写真メタデータの読み書き
  • tus
    再開可能なアップロード
AI サービス
  • Python + FastAPI
    ステートレスな HTTP サービス
  • ONNX Runtime
    顔・文字認識の推論
  • PyTorch
    Chinese-CLIP
  • InsightFace
    SCRFD + ArcFace
  • RapidOCR
    PP-OCR 文字認識
データ
  • PostgreSQL 17
    業務データとジョブキュー
  • pgvector
    ベクトルの保存
  • HNSW
    コサイン距離の索引
  • ip2region
    ログイン履歴の IP 位置(オフライン)
  • Nominatim
    逆ジオコーディング(無効化可)
フロントエンドと導入
  • React + TypeScript
    Web アプリ
  • Vite + Tailwind CSS
    ビルドとスタイル
  • MapLibre GL
    衛星地球儀
  • Docker Compose
    amd64 / arm64 イメージ
  • mDNS
    wanmory.local LAN ドメイン
PRIVACY

プライバシーとセキュリティ

初期設定では、写真と AI の結果はプライベートネットワーク内にとどまります。リモート AI サービスを設定した場合は、プレビュー画像がそのサーバーに送られて処理されます。

最小限のデータ

AI サービスが受け取るのは 1440 ピクセルのプレビューだけ。ファイルは一切保存せず、データベース、人名、原本にも触れません。

認証

内蔵の AI サービスは Docker の内部ネットワークにだけ存在し、接続できるのはバックエンドのみ。リモート AI サービスにトークンを設定すると、すべてのリクエストに Bearer トークンが必要になります。

権限

メンバーが削除できるのは自分がアップロードした写真だけで、管理者はすべてを管理できます。非表示にした写真は本人にしか見えず、ログイン履歴には IP から推定したおおよその位置が表示されます。

原本は読み取り専用

NAS のフォルダーは :ro で読み取り専用にマウントでき、アプリが中のファイルを変更・削除することはできません。

オフラインでも使える

モデルのダウンロード後は認識も検索もローカルで完結。逆ジオコーディングや衛星画像など、ネット接続が必要な機能はオフにできます。

モデルのライセンス

InsightFace の顔認識モデルは非商用利用に限られます。モデルはイメージに含めず、ダウンロード前に設定画面で各モデルのライセンスを表示します。