Misskey 本体の AiService.detectSensitive(nsfwjs + @tensorflow/tfjs-node による NSFW 推論)を切り出した、独立 HTTP サイドカーサービス。
切り出して嬉しいのは ネイティブ ML スタック(tfjs-node / libtensorflow、モデルのメモリ常駐、x64 の avx2+fma CPU 制約、glibc 依存)の隔離 であり、本サービスはそこだけに徹する。画像の正規化(リサイズ・回転・透過塗りつぶし)や動画フレーム抽出は Misskey 本体に残し、本サービスは 正規化済み画像バイトを受け取り nsfwjs の生の予測値をそのまま返す。しきい値判定(sensitive / porn)も本体側に残す。
複数の正規化済み画像を一括推論する。
| Content-Type | multipart/form-data |
| Authorization | Bearer <token>(config.apiKey 設定時のみ要求) |
| Body | 各パートに画像バイナリ(フィールド名は任意、順序を保持)。パートの Content-Type は image/png / image/jpeg / image/gif / image/bmp |
成功(200):
{
"success": true,
"result": {
"results": [
{ "success": true, "predictions": [{ "className": "Neutral", "probability": 0.98 }, "..."] },
{ "success": false, "error": { "code": "IMAGE_DECODE_FAILED", "message": "..." } }
]
}
}results の順序はリクエストパーツの順序と一致する。1 枚でも失敗しても全体は 200 を返す(部分成功)。
各パーツのサイズ上限は maxBinarySize を個別適用。
失敗(4xx)はリクエスト全体に問題がある場合のみ:
| code | status | 意味 |
|---|---|---|
AUTHENTICATION_REQUIRED |
401 | トークン欠落 / 不一致 |
INVALID_REQUEST |
400 | パーツなし / multipart パース失敗 |
UNSUPPORTED_MEDIA_TYPE |
415 | Content-Type が multipart/form-data 以外 |
REQUEST_TOO_LARGE |
413 | Content-Length が maxBodySize 超過、パーツ数が maxParts 超過、または画像 dimensions が上限超過 |
パーツ個別の失敗(IMAGE_DECODE_FAILED、REQUEST_TOO_LARGE 等)は results[i].error.code に入り、HTTP は 200。
動作確認:
curl -X POST localhost:3000/v1/detect-images \
-F 'a=@frame1.png;type=image/png' \
-F 'b=@frame2.png;type=image/png'--config <path> または環境変数 SENSITIVE_DETECTOR_CONFIG で .mjs / .cjs のパスを指定する(default export が設定)。スキーマと既定値は config.example.mjs を参照。
port/socket: どちらか一方必須。host:port待ち受け時の bind ホスト。既定127.0.0.1(ローカルのみ)。外部公開する場合のみ0.0.0.0を明示する(Docker はconfig.docker.mjsで0.0.0.0指定済み)。- 移行メモ: 既定が
0.0.0.0から127.0.0.1に変わった。port待ち受けで別ホスト/別コンテナから到達させていた既存利用者は、host: '0.0.0.0'(や特定の bind アドレス)を明示する必要がある。Docker 利用は変更不要。
- 移行メモ: 既定が
modelDir: 必須。nsfwjs モデルディレクトリ。apiKey: 静的 Bearer token。portで TCP 待ち受けする場合はapiKeyが必須。allowUnauthenticatedTcp:portでapiKeyなしを許すためのフラグ。開発用・外部から到達不能な環境以外では使わない。maxBinarySize(1MB) /maxImageWidth(299) /maxImageHeight(299) /maxImagePixels(89401) /maxParts(10) /maxBodySize(12MB) /maxConcurrentJobs(2) /requestTimeoutMs(60000)。
pnpm install # tfjs-node のネイティブビルドを含む
pnpm run build # tsdown(高速 JS 出力。依存は external)
pnpm run typecheck # core を build してから各 package を型チェック
pnpm run lint # biome
pnpm run test:unit # 純粋ロジック
pnpm run test:integration # 実モデルロード+実 classify(CPU/モデルが無ければ skip)
# ローカル起動(config に modelDir を設定)
pnpm --filter @misskey-sensitive-detector/server dev -- --config ./config.dev.mjs統合テストは SENSITIVE_DETECTOR_TEST_MODEL_DIR でモデルディレクトリを上書きできる
(既定: /home/osamu/develop/misskey/packages/backend/nsfw-model)。CPU が avx2+fma 非対応、
またはモデルが無い環境では自動的に skip する。
packages/core(@misskey-sensitive-detector/core): 推論エンジン。重いネイティブ実依存(tfjs-node / nsfwjs)はここに集約。apps/server(@misskey-sensitive-detector/server): 薄い HTTP 層(Hono + pino)。
モデル(nsfw-model/)はイメージに同梱され /models へ焼き込まれる。config だけ実行時にマウントする。
ベースは node:22-bookworm-slim(glibc)。フレーム抽出は本体側に残すため ffmpeg は不要。
cp .env.example .env # SENSITIVE_DETECTOR_API_KEY を埋める(必須)
docker compose up -d --buildホスト側は既定で 3009 に公開する(.env の HOST_PORT で変更可。Misskey 本体が 3000 を使うため
ずらしてある)。設定は config.docker.mjs をマウントし、apiKey のみ環境変数から注入する。
ヘルスチェック・自動再起動・リソース制限・ログ rotation・最小権限実行は compose 側で設定済み。
docker build -t sensitive-detector .
docker run --rm -p 127.0.0.1:3009:3009 \
-v /path/to/config.mjs:/config/config.mjs \
sensitive-detectorconfig.mjs 内で modelDir: '/models'、port: 3009 を指定する。別のモデルを使う場合のみ
-v /path/to/nsfw-model:/models:ro で同梱モデルを上書きする。
本体側で AiService.detectSensitive を本サービスへの HTTP 呼び出しに置換する。FileInfoService は
現行どおり正規化・フレーム抽出・judgePrediction・集約を担い、各フレームを 299×299 PNG に正規化して
/v1/detect-images に一括送信する。これにより現行挙動が完全に保存される。