デプロイガイド¶
標準構成 (Docker Compose)¶
クイックスタート を参照。すべてのサービスが Docker Compose で管理される標準的な構成。
TCP 構成 (docker-compose.yml)¶
従来の構成。nginx が各サービスに TCP で接続する。docker-compose.yml.example をコピーして使用。
UDS 構成 (docker-compose.prod.yml) — 推奨¶
Client → nginx → (UDS) → backend / s3proxy-deliverer
→ (volume) → frontend (静的ファイル)
→ (UDS) → PostgreSQL / Valkey
全サービス間通信を Unix Domain Socket で統一した本番推奨構成:
- PostgreSQL / Valkey: UDS 接続(TCP ポート無効化)
- backend / s3proxy-deliverer: nginx ↔ app 間を UDS
- frontend: ビルド済み静的ファイルを nginx が直接配信(Vite プロセス不要)
- メリット: コンテナ再作成時に nginx 再起動不要、ネットワーク攻撃面の削減、わずかなレイテンシ改善
本番更新手順¶
更新手順は クイックスタート > 本番更新手順 を参照してください。
Docker を使わない場合¶
Docker を使わずに各サービスを直接ホスト上で動かす構成。
必要なもの¶
| サービス | バージョン |
|---|---|
| Python | 3.12+ |
| Node.js | 20+ |
| PostgreSQL | 17+ |
| Valkey (or Redis) | 8+ |
| nekono3s | latest |
| s3proxy-deliverer | latest |
| nginx (or Cloudflared) | - |
nekono3s¶
nekono3s は S3 互換のオブジェクトストレージ。コンテナイメージからバイナリを取り出すか、ソースからビルドして使う。
# データディレクトリを作成
mkdir -p /var/lib/nekono3s
# 環境変数を設定して起動
export S3_ACCESS_KEY_ID=nekonoverse
export S3_SECRET_ACCESS_KEY=<強力なパスワード>
export S3_STORAGE_PATH=/var/lib/nekono3s
export S3_REGION=us-east-1
export S3_XATTR_JCLOUDS_COMPAT=true
# nekono3s を起動 (デフォルト: ポート 8080)
nekono3s
xattr サポート
ストレージのファイルシステムが xattr をサポートしている必要がある(ext4, XFS, Btrfs 等)。tmpfs や一部の NFS では動作しない。
s3proxy-deliverer¶
s3proxy-deliverer は nekono3s のデータディレクトリを読み取り専用で共有し、認証なしでファイルを配信する。
# nekono3s と同じデータディレクトリを指定
export STORAGE_ROOT=/var/lib/nekono3s
# s3proxy-deliverer を起動 (デフォルト: ポート 80)
s3proxy-deliverer
ポートの変更
非 root で実行する場合、ポート 80 はバインドできない。--port 8081 等で変更し、nginx/Cloudflared のプロキシ先を合わせること。
バックエンド¶
cd backend
# 依存関係をインストール
pip install -r requirements.txt
# 環境変数を設定
export DATABASE_URL=postgresql+asyncpg://nekonoverse:<password>@localhost:5432/nekonoverse
export VALKEY_URL=valkey://localhost:6379/0
export DOMAIN=your-domain.example
export SECRET_KEY=<ランダムな文字列>
export DEBUG=false
export S3_ENDPOINT_URL=http://localhost:8080
export S3_ACCESS_KEY_ID=nekonoverse
export S3_SECRET_ACCESS_KEY=<nekono3sと同じパスワード>
export S3_BUCKET=nekonoverse
export S3_REGION=us-east-1
# DBマイグレーション
alembic upgrade head
# アプリケーション起動
uvicorn app.main:app --host 127.0.0.1 --port 8000
# ワーカー起動 (別ターミナル)
python -m app.worker.main
フロントエンド¶
cd frontend
npm install
npm run build
# 静的ファイルを nginx で配信するか、Vite プレビューサーバーを使う
npx vite preview --host 127.0.0.1 --port 3000
nginx 設定¶
upstream backend {
server 127.0.0.1:8000;
}
upstream frontend {
server 127.0.0.1:3000;
}
upstream s3proxy-deliverer {
server 127.0.0.1:8081; # s3proxy-deliverer のポート
}
server {
listen 80;
server_name your-domain.example;
client_max_body_size 10M;
# SSE streaming — バッファリング無効、長タイムアウト
location /api/v1/streaming/ {
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
chunked_transfer_encoding off;
}
# API routes
location /api/ {
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# ActivityPub routes
location /.well-known/ { proxy_pass http://backend; proxy_set_header Host $host; }
location /users/ { proxy_pass http://backend; proxy_set_header Host $host; }
location /inbox { proxy_pass http://backend; proxy_set_header Host $host; }
location /nodeinfo/ { proxy_pass http://backend; proxy_set_header Host $host; }
location /oauth/ { proxy_pass http://backend; proxy_set_header Host $host; }
location /manifest.webmanifest { proxy_pass http://backend; proxy_set_header Host $host; }
# Notes: AP リクエストはバックエンドへ、ブラウザはフロントエンドへ
location /notes/ {
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
if ($http_accept ~* "application/(activity\+json|ld\+json)") {
proxy_pass http://backend;
break;
}
proxy_pass http://frontend;
}
# Media files
location /media/ {
rewrite ^/media/(.*)$ /nekonoverse/$1 break;
proxy_pass http://s3proxy-deliverer;
proxy_set_header Host $host;
proxy_hide_header x-amz-request-id;
proxy_hide_header x-amz-id-2;
add_header Cache-Control "public, max-age=86400, immutable";
proxy_buffering on;
}
# Frontend
location / {
proxy_pass http://frontend;
proxy_set_header Host $host;
}
}
systemd サービス例¶
各サービスを systemd で管理する場合の例:
# /etc/systemd/system/nekonoverse-backend.service
[Unit]
Description=Nekonoverse Backend
After=network.target postgresql.service valkey.service
[Service]
Type=exec
User=nekonoverse
WorkingDirectory=/opt/nekonoverse/backend
EnvironmentFile=/opt/nekonoverse/.env
ExecStart=/opt/nekonoverse/backend/.venv/bin/uvicorn app.main:app --host 127.0.0.1 --port 8000
Restart=on-failure
[Install]
WantedBy=multi-user.target
同様に nekonoverse-worker.service、nekono3s.service、s3proxy-deliverer.service を作成する。
Cloudflared (Cloudflare Tunnel) を使う場合¶
nginx の代わりに Cloudflare Tunnel を使ってリバースプロキシを行う構成。TLS 終端やキャッシュを Cloudflare が担当する。
メリット¶
- TLS 証明書の管理が不要(Cloudflare が自動管理)
- サーバーのポートを外部に公開する必要がない
- Cloudflare の CDN キャッシュが利用可能
- DDoS 対策が組み込まれている
構成図¶
セットアップ¶
1. Cloudflare Tunnel の作成¶
# cloudflared をインストール
# https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/
# ログインしてトンネルを作成
cloudflared tunnel login
cloudflared tunnel create nekonoverse
2. 設定ファイル¶
# ~/.cloudflared/config.yml
tunnel: <tunnel-id>
credentials-file: ~/.cloudflared/<tunnel-id>.json
ingress:
# API routes
- hostname: your-domain.example
path: /api/.*
service: http://localhost:8000
# ActivityPub routes
- hostname: your-domain.example
path: /.well-known/.*
service: http://localhost:8000
- hostname: your-domain.example
path: /users/.*
service: http://localhost:8000
- hostname: your-domain.example
path: /notes/.*
service: http://localhost:8000
- hostname: your-domain.example
path: /inbox
service: http://localhost:8000
- hostname: your-domain.example
path: /nodeinfo/.*
service: http://localhost:8000
- hostname: your-domain.example
path: /oauth/.*
service: http://localhost:8000
- hostname: your-domain.example
path: /manifest.webmanifest
service: http://localhost:8000
# Media files -> s3proxy-deliverer
- hostname: your-domain.example
path: /media/.*
service: http://localhost:8081
originRequest:
httpHostHeader: your-domain.example
# Everything else -> frontend
- hostname: your-domain.example
service: http://localhost:3000
- service: http_status:404
メディアの URL リライト
Cloudflared は nginx のような rewrite をサポートしていない。/media/{key} → /nekonoverse/{key} のリライトが必要なため、s3proxy-deliverer の前段に軽量なリバースプロキシを挟むか、以下のいずれかの方法で対応する。
メディア URL リライトの対応方法¶
Cloudflared にはパスの書き換え機能がないため、/media/ → /nekonoverse/ のリライトに別途対応が必要。
方法 A: Cloudflare Transform Rules (推奨)
Cloudflare ダッシュボードの Rules > Transform Rules > Rewrite URL で設定:
- 条件:
URI Pathstarts with/media/ - 書き換え: Dynamic —
concat("/nekonoverse", substring(http.request.uri.path, 6))
この方法では Cloudflare のエッジでリライトが行われるため、追加のプロキシが不要。
方法 B: Caddy をローカルリバースプロキシとして使う
s3proxy-deliverer の前段に Caddy を置いてリライトを行う:
# Caddyfile
:8082 {
handle /media/* {
uri strip_prefix /media
rewrite * /nekonoverse{uri}
reverse_proxy localhost:8081
}
}
Cloudflared の ingress で /media/.* のサービスを http://localhost:8082 に変更する。
3. DNS 設定¶
4. 起動¶
Docker Compose で使う場合¶
docker-compose.yml.example の末尾にコメントアウトされた cloudflared サービスがあります。nginx サービスを削除(またはコメントアウト)し、cloudflared のコメントを外して使います。
Cloudflare Zero Trust ダッシュボードの Networks > Tunnels でトンネルを作成し、Public Hostname のルーティングを設定する。CLOUDFLARE_TUNNEL_TOKEN は .env に記載する。
環境変数
Cloudflared を使う場合、バックエンドの DEBUG=false を設定すること。Cloudflare が HTTPS を終端し X-Forwarded-Proto: https をセットするため、バックエンドは HTTPS 前提で動作する。
メール設定 (SMTP)¶
メール認証とパスワードリセット機能を有効にするには、SMTP サーバーの設定が必要。未設定の場合、メール機能は無効化され、既存の動作に影響はない。
環境変数¶
.env に追加:
SMTP_HOST=smtp.resend.com
SMTP_PORT=465
SMTP_USER=resend
SMTP_PASSWORD=re_xxxxxxxxxxxx
SMTP_FROM=noreply@yourdomain.com
SMTP_SECURITY=ssl
| 変数 | 説明 | デフォルト |
|---|---|---|
SMTP_HOST |
SMTP サーバーのホスト名 | (未設定=メール無効) |
SMTP_PORT |
ポート番号 | 587 |
SMTP_USER |
認証ユーザー名 | — |
SMTP_PASSWORD |
認証パスワードまたは API キー | — |
SMTP_FROM |
送信元メールアドレス | noreply@{DOMAIN} |
SMTP_SECURITY |
接続方式 | starttls |
SMTP_SECURITY の選択¶
| 値 | ポート | 説明 | 対応サービス例 |
|---|---|---|---|
starttls |
587 | 平文接続後に STARTTLS でTLS昇格 | Gmail, Mailgun, SendGrid |
ssl |
465 | 接続時からTLS (暗黙TLS/SMTPS) | Resend, Amazon SES |
none |
25 | 暗号化なし (非推奨) | ローカル開発用 |
主要サービスの設定例¶
Resend:
SMTP_HOST=smtp.resend.com
SMTP_PORT=465
SMTP_USER=resend
SMTP_PASSWORD=re_xxxxxxxxxxxx
SMTP_FROM=noreply@yourdomain.com
SMTP_SECURITY=ssl
Gmail (アプリパスワード):
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=you@gmail.com
SMTP_PASSWORD=xxxx-xxxx-xxxx-xxxx
SMTP_FROM=you@gmail.com
SMTP_SECURITY=starttls
動作¶
- メール認証: ユーザー登録時に認証メールを自動送信。未認証でもログイン・投稿は可能(Mastodon互換)。設定画面に認証状態を表示
- パスワードリセット: ログイン画面の「パスワードを忘れた場合」からリセットメールを送信
- メールアドレス変更: 設定画面から確認メール付きで変更可能
- データエクスポート完了通知: エクスポートが完了するとダウンロードリンク付きメールを送信
- レート制限: 同一IPからのメール送信は15分間に5通まで。トークン検証は15分間に10回まで
- メールキュー: Valkey ベースの非同期キュー。最大5回リトライ、失敗はデッドレターキューへ
Cloudflare Turnstile CAPTCHA¶
登録フォームに Cloudflare Turnstile の CAPTCHA を追加できる。未設定時は CAPTCHA なしで動作する。
セットアップ¶
- Cloudflare ダッシュボード → Turnstile → サイトを追加
- サイトのドメインを登録し、Site Key と Secret Key を取得
.envに追加:
- コンテナを再起動すれば登録フォームに CAPTCHA が表示される
データエクスポート¶
ユーザーが自分のデータをZIPアーカイブとしてエクスポートできる機能。GDPR のデータポータビリティ要件やサーバー移行に対応。
エクスポート内容¶
| ファイル | 内容 | 形式 |
|---|---|---|
actor.json |
プロフィール情報 | ActivityPub Actor |
outbox.json |
全投稿 | ActivityPub OrderedCollection |
following_accounts.csv |
フォローリスト | Mastodon互換CSV |
followers_accounts.csv |
フォロワーリスト | CSV |
bookmarks.csv |
ブックマーク | ノートURL一覧 |
blocked_accounts.csv |
ブロックリスト | CSV |
muted_accounts.csv |
ミュートリスト | CSV |
media/ |
アップロードしたメディア | 元ファイル |
動作¶
- 設定 → アカウント → データエクスポート からエクスポートを開始
- バックグラウンドのワーカープロセスで ZIP を生成し、S3 にアップロード
- 完了後、設定画面からダウンロード可能(SMTP 設定時はメール通知も送信)
- ZIPの有効期限は7日間
- 24時間に1回のレート制限あり
- ワーカーは最大3回リトライ、失敗時はエラーメッセージを表示
API¶
| メソッド | パス | 説明 |
|---|---|---|
POST |
/api/v1/export |
エクスポート開始(24時間に1回) |
GET |
/api/v1/export |
最新エクスポートのステータス取得 |
GET |
/api/v1/export/{id}/download |
ZIPダウンロード(本人のみ) |
FRONTEND_URL の設定
メール通知のダウンロードリンクに FRONTEND_URL を使用するため、worker コンテナにも FRONTEND_URL 環境変数が必要。docker-compose.prod.yml / docker-compose.yml の worker サービスに FRONTEND_URL: https://${DOMAIN} を追加すること。
顔検出サーバーを別マシンに分離する¶
face-detect/(git submodule)は顔検出マイクロサービスで、アップロードされた画像から顔の位置を検出し、フォーカルポイントを自動設定する。GPU マシンに分離することで、メインサーバーの負荷を分散できる。
検出は2段階で行われる(DETECTION_MODE で変更可能):
- アニメ顔検出 (
deepghs/anime_face_detectionYOLO ONNX, GPU) — F1 0.97、VRAM ~50MB - 実写顔検出 (MTCNN, PyTorch, GPU) — アニメ顔が見つからない場合のフォールバック、VRAM ~200MB
| モード | 環境変数 DETECTION_MODE |
動作 | VRAM |
|---|---|---|---|
| 自動 (デフォルト) | auto |
アニメ→実写フォールバック | ~250MB |
| アニメのみ | anime |
アニメ顔のみ検出 | ~50MB |
| 実写のみ | real |
実写顔のみ検出 | ~200MB |
オプション機能
顔検出は FACE_DETECT_URL が未設定なら完全にスキップされる。サービスがダウンしていてもアップロードは正常に動作する(silent fail)。ユーザーはフロントエンドの FocalPointPicker から手動設定も可能。
構成図¶
GPU サーバー側のセットアップ¶
直接実行¶
# face-detect/ ディレクトリをコピー
scp -r face-detect/ gpu-server:/opt/face-detect/
# GPU マシンで起動
cd /opt/face-detect
pip install -r requirements.txt # torch, facenet-pytorch, onnxruntime-gpu, huggingface-hub, etc.
# 検出モードを指定して起動 (デフォルト: auto)
DETECTION_MODE=auto uvicorn main:app --host 0.0.0.0 --port 8100
# アニメのみモード (MTCNNをロードしない、VRAM節約)
DETECTION_MODE=anime uvicorn main:app --host 0.0.0.0 --port 8100
Docker¶
FROM pytorch/pytorch:2.x-cuda12.x-runtime
COPY . /app
WORKDIR /app
RUN pip install -r requirements.txt
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8100"]
systemd サービス例¶
# /etc/systemd/system/face-detect.service
[Unit]
Description=Nekonoverse Face Detection
After=network.target
[Service]
Type=exec
User=nekonoverse
WorkingDirectory=/opt/face-detect
Environment=DETECTION_MODE=auto
ExecStart=/opt/face-detect/.venv/bin/uvicorn main:app --host 0.0.0.0 --port 8100
Restart=on-failure
[Install]
WantedBy=multi-user.target
メインサーバー側の設定¶
.env に追加:
Docker Compose の場合は docker-compose.yml の app サービスの environment に追加する。
ネットワーク¶
| 方式 | 設定例 | 備考 |
|---|---|---|
| Tailscale (推奨) | http://100.x.x.x:8100 |
設定不要で暗号化 |
| VPN / プライベートネットワーク | http://192.168.x.x:8100 |
ファイアウォールで 8100 を制限 |
セキュリティ
顔検出 API は認証なしのため、公開ネットワークに露出させないこと。Tailscale やファイアウォールでメインサーバーからのアクセスのみ許可する。
動作確認¶
# GPU マシンのヘルスチェック
curl http://<GPU_IP>:8100/health
# → {"status":"ok","device":"cuda","detection_mode":"auto","models":{"anime":true,"real":true}}
# メインサーバーからの疎通確認
curl http://<GPU_IP>:8100/health
API 仕様¶
| エンドポイント | メソッド | 説明 |
|---|---|---|
/health |
GET | ヘルスチェック。models でアニメ/実写の有効状況を返す |
/object-detection |
POST | 顔検出(アニメ優先→実写フォールバック)。HF Inference API 互換 |
レスポンス例 (/object-detection):
[
{
"label": "anime_face",
"score": 0.8,
"box": { "xmin": 30, "ymin": 20, "xmax": 70, "ymax": 60 }
}
]
label は anime_face(アニメ検出)または face(実写検出)。バックエンドはバウンディングボックスの中心座標を正規化 [-1, 1] のフォーカルポイントに変換して drive_files.focal_x / focal_y に保存する(label は参照しない)。
画像自動タグ付けサービス (neko-vision)¶
neko-vision/(git submodule)は Ollama 連携の画像自動タグ付け/キャプション生成マイクロサービス。アップロードされた画像やリモートノートの添付画像に対して、日本語のタグ(3〜7個)とキャプションを自動付与する。ノート本文やリプライツリーの文脈をコンテキストとして渡すことで、固有名詞の捕捉精度が向上する。
オプション機能
画像タグ付けは NEKO_VISION_URL が未設定なら完全にスキップされる。サービスがダウンしていても画像アップロードや連合は正常に動作する(silent fail)。
構成図¶
バックエンドは画像のアップロード時やリモートノート取得時に Valkey ジョブキューにエンキューし、ワーカーが非同期で neko-vision API を呼び出す。タグ付け完了後は SSE でクライアントに通知する。
要件¶
| 項目 | 要件 |
|---|---|
| GPU | NVIDIA GPU + nvidia-container-toolkit(Ollama 用) |
| VRAM | 4GB 以上推奨(gemma3:4b) |
| ディスク | ~3GB(モデル重み) |
セットアップ: Docker Compose 同梱(推奨)¶
メインの docker-compose.yml / docker-compose.prod.yml に neko-vision + Ollama のサービス定義がコメントアウトされている。以下の手順で有効化する:
- docker-compose ファイルの
neko-visionとollamaサービスのコメントを解除 volumesセクションのollama-dataのコメントを解除- サービスを起動し、Ollama にモデルをダウンロード:
.envに環境変数を追加:
セットアップ: 別マシンに分離¶
GPU マシンを分離する場合は neko-vision/docker-compose.yml.example を使用する:
# GPU マシンで
cd neko-vision
cp docker-compose.yml.example docker-compose.yml
docker compose up -d
# 初回のみ: モデルダウンロード
docker compose exec ollama ollama pull gemma3:4b
ポート 8004 で neko-vision API が公開される。
直接実行¶
# GPU マシンで Ollama をインストール・起動
# https://ollama.com/download
ollama pull gemma3:4b
# neko-vision を起動
cd neko-vision
pip install -r requirements.txt
OLLAMA_URL=http://localhost:11434 OLLAMA_MODEL=gemma3:4b \
uvicorn main:app --host 0.0.0.0 --port 8004
systemd サービス例¶
# /etc/systemd/system/neko-vision.service
[Unit]
Description=Nekonoverse Vision Tagging
After=network.target ollama.service
[Service]
Type=exec
User=nekonoverse
WorkingDirectory=/opt/neko-vision
Environment=OLLAMA_URL=http://localhost:11434
Environment=OLLAMA_MODEL=gemma3:4b
ExecStart=/opt/neko-vision/.venv/bin/uvicorn main:app --host 0.0.0.0 --port 8004
Restart=on-failure
[Install]
WantedBy=multi-user.target
メインサーバー側の設定¶
.env に追加:
# TCP(リモートマシン)
NEKO_VISION_URL=http://<GPUマシンのIP>:8004
# UDS(同一マシン)
NEKO_VISION_URL=http://localhost
NEKO_VISION_UDS=/var/run/neko-vision/uvicorn.sock
Docker Compose の場合は docker-compose.yml の app / worker サービスの environment に追加する。
ネットワーク¶
| 方式 | 設定例 | 備考 |
|---|---|---|
| Tailscale (推奨) | http://100.x.x.x:8004 |
設定不要で暗号化 |
| VPN / プライベートネットワーク | http://192.168.x.x:8004 |
ファイアウォールで 8004 を制限 |
セキュリティ
neko-vision API は認証なしのため、公開ネットワークに露出させないこと。Tailscale やファイアウォールでメインサーバーからのアクセスのみ許可する。
動作確認¶
# ヘルスチェック
curl http://<GPU_IP>:8004/health
# → {"status":"ok","model":"gemma3:4b","ollama_url":"http://ollama:11434","ollama_connected":true}
API 仕様¶
| エンドポイント | メソッド | 説明 |
|---|---|---|
/health |
GET | ヘルスチェック。Ollama 接続状態とモデル情報を返す |
/tag |
POST | 画像にタグとキャプションを付与する |
リクエスト例 (POST /tag):
{
"image": "<base64エンコードされた画像データ>",
"text": "投稿本文(オプション)",
"context": ["リプライツリーの親ノート本文(古い順、オプション)"]
}
レスポンス例:
text と context はオプション。指定すると投稿文脈を考慮してより的確なタグとキャプションを生成する。context はリプライツリーの親ノート本文を古い順に最大5件まで渡せる。
再タグ付け CLI¶
モデル更新やプロンプト改善後に既存画像を再判定する CLI ツール:
# 特定日以前にタグ付けされた画像を再判定
docker compose exec app python -m scripts.vision_retag --before 2026-03-01
# 全画像を再判定(件数確認のみ)
docker compose exec app python -m scripts.vision_retag --all --dry-run
# 未タグ付けの画像のみ(100件まで)
docker compose exec app python -m scripts.vision_retag --untagged --limit 100
| オプション | 説明 |
|---|---|
--before YYYY-MM-DD |
指定日以前にタグ付けされた画像 + 未タグ付け画像を再判定 |
--all |
タグ付け済みの全画像を再判定 |
--untagged |
未タグ付けの画像のみエンキュー |
--dry-run |
件数確認のみ(エンキューしない) |
--limit N |
処理件数を制限(0 = 無制限) |
メディアタイムライン¶
neko-vision でタグ付けされた画像は /media ページのメディアタイムラインで活用される:
- 3カラムの正方形サムネイルグリッド(モバイルは2カラム)
- ホバーで vision タグとキャプションをオーバーレイ表示
- タグ・キャプション・ノート本文での全文検索に対応
- クリックでノート詳細を表示