Skip to content

センシティブ判定を外部サービス扱いにする #16804

Description

@syuilo

Summary

summalyみたいにURLを設定してそこにリクエストする形式にする

Purpose

  • nsfwjsはスクリプトサイズ的にもメモリ使用量的にもヘビーなため、外部化できれば本体の必要要件を緩和できる
  • センシティブ判定機能を使用しないサーバーで無駄にストレージ/メモリを消費することがなくなる
  • nsfwjsに起因するトラブル(Misskeyのインストールに失敗する、メモリリークする等)に影響を受けなくなる
  • APIさえ同じであれば、nsfwjs以外の(より効率的な)実装を運営者の好みに応じて選択することもできる

Do you want to implement this feature yourself?

  • Yes, I will implement this by myself and send a pull request

Activity

  1. syuilo commented on Nov 17, 2025

    @syuilo
    MemberAuthor

    ご意見を募集しています

  2. samunohito commented on Nov 17, 2025

    @samunohito
    Member

    nsfwjsはスクリプトサイズ的にもメモリ使用量的にもヘビーなため、外部化できれば本体の必要要件を緩和できる
    nsfwjsに起因するトラブル(Misskeyのインストールに失敗する、メモリリークする等)に影響を受けなくなる

    一応確認ですが、Misskey本体から完全に切り離す方向性です?

  3. syuilo commented on Nov 17, 2025

    @syuilo
    MemberAuthor

    yes

  4. KisaragiEffective commented on Nov 17, 2025

    @KisaragiEffective
    Collaborator

    良さそう、例のDockerコンテナが膨れるのも割と解決できそう

  5. syuilo commented on Nov 18, 2025

    @syuilo
    MemberAuthor

    やるか

  6. samunohito commented on Nov 18, 2025

    @samunohito
    Member

    実装を進める傍ら、下記も併せて考えたい気持ちがある(今使ってる人が混乱しないようなフォロー)

    • Misskey本体コンパネのセンシティブ判定設定部分にMkInfoを置いて外部化されたこと、分離した機能のセットアップがべと必要なことを明記
    • CHANGELOG.mdのNote項目への記載を厚めに

    あと、分離先でも…

    • 分離したリポジトリ側のREADME拡充(セットアップ方法、Misskey本体への設定方法など)
  7. eternal-flame-AD commented on Nov 18, 2025

    @eternal-flame-AD
    SponsorContributor

    I suggest if possible this be done with an asynchronous API. I.e. one for pulling a list of media without a classification, one API for setting the results of the classification, both are easy to do with DB and decouples heavy computation of classification from the state management loop.

    Otherwise it might be a little .. shifting load to something "external" but not really reducing latency or increasing flexibility.

  8. eternal-flame-AD commented on Nov 18, 2025

    @eternal-flame-AD
    SponsorContributor

    I think I meant partly considering relaxing the consistency requirement here, if the design is such that every single media has to come in, block the current federation job, get a result from a likely computationally heavy function (whether built into misskey or an external synchronous API), it likely does not reduce the process latency and causes more availability problem (what to do if the API call fails?).

    Since there is no 100% recall algorithm for detecting "sensitive media" nor will there ever be in the near future, I think it is okay to let a post go through first and then later we can update its media to be sensitive if needed asynchronously, like most moderation work is - the goal is be scalable and minimize disruption.

  9. syuilo commented on Nov 18, 2025

    @syuilo
    MemberAuthor

    センシティブフラグの付与を非同期的にするかどうかは外部サービス化とは異なる関心なので、別Issueの方が良さそうです
    ここで非同期化の懸念について少し書くと、非同期の場合判定が完了する前にユーザーが投稿できてしまうことにならないかしらという点です

  10. eternal-flame-AD commented on Nov 18, 2025

    @eternal-flame-AD
    SponsorContributor

    It's okay we can leave asynchronous on hold first but I believe fault tolerance should be a concern here - what happens if the API call fails? Should we just stop everything or have some other strategy?

  11. 5 remaining items

  12. syuilo commented on Nov 24, 2025

    @syuilo
    MemberAuthor

    MPがない

  13. samunohito commented on Jun 2, 2026

    @samunohito
    Member

    https://github.com/misskey-dev/sensitive-detector
    まだ何もありませんが、↑を作りました。ここにnsfw検出用のサーバ実装を生やしていきます。

  14. kakkokari-gtyih commented on Jun 2, 2026

    @kakkokari-gtyih
    Member

    良さそう
    と思っていたけど、センシティブメディア判定を行う場合は対象ファイルを一個アップロードするたびに外部にHTTPリクエストが発生することになるので、そのオーバーヘッドが大丈夫そうかをやや懸念している

    例えばnsfwjsの採用をやめてネイティブバイナリ(napi)のセンシティブ判定ライブラリを作って入れるとか

  15. samunohito commented on Jun 2, 2026

    @samunohito
    Member

    オーバーヘッドがあることは確実ですが、非常に小さいと思っています。

    • 転送データは299x299の小さいpng(おおよそ100KBくらい?多くても300KBくらいなはず)
    • 仮に同じマシンに外部サービスを置くとして、1回のHTTPレスポンスで数ミリ秒ほどだと思われる
    • 判定対象のファイルはノート投稿と比べて多くないはず

    という所から…

  16. kakkokari-gtyih commented on Jun 2, 2026

    @kakkokari-gtyih
    Member

    画像の判定だけでなく動画の判定もあるからそっちをどうするかという問題もある

  17. samunohito commented on Jun 3, 2026

    @samunohito
    Member

    あー、前提を書いていなかった気がする
    FileInfoServiceが持っているロジックも外部化する前提であれば、動画の転送コストは考慮する必要があると思います。
    (どう迂回するか、迂回方法は安全かも考えないといけない)

    自分が考えているのは、動画からのフレーム抽出・ピックアップと画像の正規化はMisskey側に残し、外部サービス化するのは正規化された画像から判定する箇所のみ…という構成でした。
    なので、送られてくるのは画像オンリー -> #16804 (comment) という意見となった次第です(冒頭の課題をコンパクトに解消するというのがコンセプト)

  18. kakkokari-gtyih commented on Jun 3, 2026

    @kakkokari-gtyih
    Member

    動画の画像を切り出して送信してnsfw判定をするというのは私も同じ認識だけど、そうなると動画については切り出されたフレーム分だけリクエストが発生するよね、ということを言いたかった

  19. samunohito commented on Jun 3, 2026

    @samunohito
    Member

    なるほど。

    const [outDir, disposeOutDir] = await createTempDir();
    try {
    const command = FFmpeg()
    .input(source)
    .inputOptions([
    '-skip_frame', 'nokey', // 可能ならキーフレームのみを取得してほしいとする(そうなるとは限らない)
    '-lowres', '3', // 元の画質でデコードする必要はないので 1/8 画質でデコードしてもよいとする(そうなるとは限らない)
    ])
    .noAudio()
    .videoFilters([
    {
    filter: 'select', // フレームのフィルタリング
    options: {
    e: 'eq(pict_type,PICT_TYPE_I)', // I-Frame のみをフィルタする(VP9 とかはデコードしてみないとわからないっぽい)
    },
    },
    {
    filter: 'blackframe', // 暗いフレームの検出
    options: {
    amount: '0', // 暗さに関わらず全てのフレームで測定値を取る
    },
    },
    {
    filter: 'metadata',
    options: {
    mode: 'select', // フレーム選択モード
    key: 'lavfi.blackframe.pblack', // フレームにおける暗部の百分率(前のフィルタからのメタデータを参照する)
    value: '50',
    function: 'less', // 50% 未満のフレームを選択する(50% 以上暗部があるフレームだと誤検知を招くかもしれないので)
    },
    },
    {
    filter: 'scale',
    options: {
    w: 299,
    h: 299,
    },
    },
    ])
    .format('image2')
    .output(join(outDir, '%d.png'))
    .outputOptions(['-vsync', '0']); // 可変フレームレートにすることで穴埋めをさせない
    const results: ReturnType<typeof judgePrediction>[] = [];
    let frameIndex = 0;
    let targetIndex = 0;
    let nextIndex = 1;
    for await (const path of this.asyncIterateFrames(outDir, command)) {
    try {
    const index = frameIndex++;
    if (index !== targetIndex) {
    continue;
    }
    targetIndex = nextIndex;
    nextIndex += index; // fibonacci sequence によってフレーム数制限を掛ける
    const result = await this.aiService.detectSensitive(path);
    if (result) {
    results.push(judgePrediction(result));
    }
    } finally {
    fs.promises.unlink(path);
    }
    }
    sensitive = results.filter(x => x[0]).length >= Math.ceil(results.length * sensitiveThreshold);
    porn = results.filter(x => x[1]).length >= Math.ceil(results.length * sensitiveThresholdForPorn);

    ↑を見る限り、

    • キーフレームが対象になっている
    • 暗すぎるフレームは除外している
    • 抽出されたキーフレーム一覧に対し、判定対象のフレーム数に制限をかけている

    という点から、膨大な量がリクエストされることはないだろうと踏んでいます。
    また、1リクエストに1ファイルのみではなく、複数のフレーム画像ファイルをまとめて送るようにするという方向での緩和も出来ると思います(コネクションのオーバヘッドという観点であれば、keep-aliveもあるし…)

  20. syuilo commented on Jun 3, 2026

    @syuilo
    MemberAuthor

    まとめて送るのは良さそう

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions