アーキテクチャ
中核となるコンテナ 3 つに mDNS サービスを加えた構成で、独立したメッセージブローカーもクラウドサービスも必要ありません。アップロードまたは NAS のフォルダーから取り込んだ写真は、メタデータ、サムネイル、セマンティックベクトル、顔、文字の順に処理され、自然言語・人物・場所で検索できるライブラリになります。
Go バックエンド、AI サービス、PostgreSQL、そして LAN ドメインを告知する mDNS
metadata、thumbnail、clip、face、ocr
Chinese-CLIP の画像・テキストベクトル(モデルティアにより 512・768・1024 次元)、ArcFace の顔特徴量は 512 次元
pgvector のコサイン距離索引。検索と顔の照合はデータベース内で行います
システム構成
ブラウザが接続するのは Go バックエンドだけ。バックエンドがアップロード、ジョブスケジューリング、サムネイル、権限管理を担い、プレビュー画像を AI サービスに渡して推論させ、結果をすべて PostgreSQL に書き込みます。AI サービスはステートレスで、データベースにアクセスせずファイルも保存しないため、より高性能なマシンに分離して配置できます。
Web アプリはビルドしてバイナリに埋め込まれているため、導入に Node は不要。起動時にデータベースのマイグレーションが自動で走り、アップグレードはイメージを入れ替えるだけです。
内蔵の AI サービスは Docker の内部ネットワークで動き、CPU で推論します。Web の設定でリモートのアドレスを入力すれば、家の GPU 搭載 PC や Mac に切り替えられます。
写真の情報、人物、設定、ジョブキュー、ベクトルはすべて PostgreSQL に保存されるため、業務データとジョブの状態はまとめてバックアップできます。写真ファイルは別途バックアップが必要です。
写真処理パイプライン
取り込んだ写真は、まずメタデータの抽出とサムネイルの生成を行い、続いてセマンティックベクトルの計算、顔と文字の認識を並列で進めます。各ステップは個別にキューに入るため、再起動しても AI サービスが落ちてもジョブは失われず、復旧後に処理が再開されます。
ingest- Web アップロードは tus で、切断後も再開可能
- /photos フォルダーを 15 分ごとにスキャン
- SHA-1 チェックサムを計算して assets に記録
metadata- ExifTool でサイズと撮影日時を取得
- GPS、カメラ、レンズ、絞りとシャッター
- タイムゾーンのない日時は設定値で解釈
thumbnail- libvips でサムネイルを生成
- さらに 1440px のプレビューを生成
- 動画は FFmpeg でフレーム抽出・変換
clip- Chinese-CLIP で 1024 次元ベクトルを計算
- 自然言語検索、自動分類
- 美的スコア、類似写真
face- SCRFD で顔を検出し 5 点で位置合わせ
- ArcFace で 512 次元の特徴量を計算
- 人物へ逐次グループ化
ocr- PP-OCR で写真内の文字を認識
- 文字で写真を検索
- 証明書・書類・スクショを判定
バックグラウンドジョブのスケジューリング
独立したメッセージブローカーはありません。ジョブキューは PostgreSQL の jobs テーブルで、写真データと同じトランザクションで書き込まれるため、写真が保存されれば必ずジョブもキューに入ります。ワーカーは SKIP LOCKED でジョブを取得し、リース機構で停滞を防ぎ、失敗時は二乗バックオフで再試行します。
-- ジョブを 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 スケジューリング
AI サービスの役割は、プレビュー画像を受け取って顔、ベクトル、文字を返すことだけ。サービスのアドレス、モデル、並列数はバックエンドが決め、AI サービスはハードウェアに応じて CUDA、CoreML / MPS、OpenVINO、CPU から選びます。
「自動」を選ぶと AI サーバーのハードウェアに応じて推奨します。使える GPU があれば高精度、CPU のみの場合はメモリ 6 GB 未満なら軽量、6 GB 以上ならバランス。
| ティア | 適したハードウェア | 顔 | セマンティック検索 |
|---|---|---|---|
| quality · 高精度 | Apple シリコン Mac、NVIDIA GPU | SCRFD-10G + ArcFace R100(antelopev2) | Chinese-CLIP ViT-H/14 · 1024 次元 |
| balanced · バランス | Intel i5 クラスの NAS | SCRFD-10G + ArcFace R100(antelopev2) | Chinese-CLIP ViT-L/14 · 768 次元 |
| lite · 軽量 | N100、Raspberry Pi 5 | SCRFD + ArcFace R50(buffalo_l) | Chinese-CLIP ViT-B/16 · 512 次元 |
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"}
}コアアルゴリズム
検索、人物、連写、AI ピックアップの裏側で使われているモデル、アルゴリズム、しきい値。AI モデルに頼るものもあれば、ベクトル計算とルールで実現しているものもあります。
Chinese-CLIP は写真と文章を同じベクトル空間に置きます。写真の取り込み時に画像ベクトルを計算しておき、検索時には「赤ちゃんの 3 歳の誕生日」のような文をテキストベクトルにして、pgvector の HNSW 索引でコサイン距離が最も近い写真を探します。
- 3 系統の結果を統合:写真内にその文字が認識されたものを最優先し、次にその地名で撮られたもの、最後に意味が最も近いものを並べます。
- 明らかに一致するものだけを返します:最も近い 1 枚とほぼ同じくらい近く、かつ一般的な写真より明確に近いことが条件で、関連の薄い写真まで返さず弱い一致を除外するのに役立ちます。
- 検索語がちょうどカテゴリ名(風景、グルメ、スクリーンショット……)なら、そのカテゴリをそのまま返します。
SCRFD で顔と 5 つの特徴点を見つけ、特徴点に合わせて 112×112 に切り出し、ArcFace で 512 次元の特徴量を算出します(正規化後の内積がそのままコサイン類似度)。写真 1 枚の処理が終わるたびに逐次グループ化するため、全体の計算完了を待つ必要はありません。
- 十分に鮮明で正面向き、大きさも十分な「良い顔」は、最も似た顔を探します。すでに人物に属している顔があれば多数決でその人物へ。どれも未所属でも似た顔が 3 枚以上あれば新しい人物を作ります。
- ぼやけた顔、横顔、小さすぎる顔は新しい人物を作らず、特定の人物によく似ている場合だけその人物に加わります。たまたま一度写っただけの通行人は登録されません。
- 複数のジョブが同時にグループ化するときは同じデータベースのアドバイザリーロックを取るため、一人が二人に分かれることはありません。カバー写真には鮮明(ラプラシアン分散)で正面(yaw)、十分に大きい一人だけの顔を選びます。
- 同じ場面を続けて撮った写真の CLIP 類似度は通常 0.94〜0.98、場面やポーズが変わると 0.7〜0.89 程度です。0.93 を類似度のしきい値にすることで、連写の可能性が高い写真を見つけやすくなります。
- おすすめとして残す 1 枚:顔があれば顔が鮮明で目を開けてカメラを見ているもの。顔がなければファイルサイズで判断します(同じカメラ・同じ設定なら、ぶれた写真は細部が少なく圧縮後のサイズが小さくなります)。
- 同じファイルが複数保存されている場合は「重複」に分けるので、選ぶ必要はありません。「残す」を押すと残りは「最近削除」に移り、完全に削除するまでは復元できます。
別のモデルは動かさず、CLIP ベクトルを再利用します。「写真作品」を表す一群の説明文と「普段のスナップ」を表す一群の説明文からそれぞれ平均ベクトルを求め、差を取ると「見栄えが良い」方向が得られます。写真のベクトルをこの方向に射影した値がスコアです。
- 順位付けにだけ使い、絶対的な点数としては扱いません。ホームのピックアップ・カルーセルはこのスコアで写真を選びます。
- 別の説明文の組で「撮り損ね」(ぶれ、白飛びなど)を判定し、見栄えの良し悪しとは分けて計算します。
使用している技術
いずれも成熟したオープンソースのコンポーネントで、独自サービスはありません。どの部品にも代替があります。
- GoAPI、ジョブキュー、サムネイルのスケジューリング
- libvipsサムネイルとプレビュー
- FFmpeg動画のフレーム抽出・変換
- ExifTool写真メタデータの読み書き
- tus再開可能なアップロード
- Python + FastAPIステートレスな HTTP サービス
- ONNX Runtime顔・文字認識の推論
- PyTorchChinese-CLIP
- InsightFaceSCRFD + ArcFace
- RapidOCRPP-OCR 文字認識
- PostgreSQL 17業務データとジョブキュー
- pgvectorベクトルの保存
- HNSWコサイン距離の索引
- ip2regionログイン履歴の IP 位置(オフライン)
- Nominatim逆ジオコーディング(無効化可)
- React + TypeScriptWeb アプリ
- Vite + Tailwind CSSビルドとスタイル
- MapLibre GL衛星地球儀
- Docker Composeamd64 / arm64 イメージ
- mDNSwanmory.local LAN ドメイン
プライバシーとセキュリティ
初期設定では、写真と AI の結果はプライベートネットワーク内にとどまります。リモート AI サービスを設定した場合は、プレビュー画像がそのサーバーに送られて処理されます。
AI サービスが受け取るのは 1440 ピクセルのプレビューだけ。ファイルは一切保存せず、データベース、人名、原本にも触れません。
内蔵の AI サービスは Docker の内部ネットワークにだけ存在し、接続できるのはバックエンドのみ。リモート AI サービスにトークンを設定すると、すべてのリクエストに Bearer トークンが必要になります。
メンバーが削除できるのは自分がアップロードした写真だけで、管理者はすべてを管理できます。非表示にした写真は本人にしか見えず、ログイン履歴には IP から推定したおおよその位置が表示されます。
NAS のフォルダーは :ro で読み取り専用にマウントでき、アプリが中のファイルを変更・削除することはできません。
モデルのダウンロード後は認識も検索もローカルで完結。逆ジオコーディングや衛星画像など、ネット接続が必要な機能はオフにできます。
InsightFace の顔認識モデルは非商用利用に限られます。モデルはイメージに含めず、ダウンロード前に設定画面で各モデルのライセンスを表示します。