開発ブログ

BLOG

フォーラムに貼られた画像を、公開してよいか Jev で判定する

フォーラムに貼られた画像を、判定だけをするモデル Jev で確かめ、通ったものだけを公開する。Jev に画像を渡す方法、性的・暴力的・差別的な画像や宣伝の見分け方とどこまで許すかの線、Jev に渡せる大きさにブラウザで縮める処理、判定がエラーになった画像も公開しない理由と、決まった画像で試した結果を書く。

公開 2026年10月19日

Metronome.fm にフォーラムをリリースしました。投稿には画像を 4 枚まで貼れるので、練習している楽譜やアプリの設定の画面を見せながら相談できます。

誰でも書ける場所には、載せてはいけない画像が貼られることもあります。そこで、アップロードされた画像は Jev で判定し、通ったものだけを公開しています。Jev は、問いに対して選択肢ごとの確率を返す、判定だけをするモデルです。投稿の文も同じ Jev で判定していて、その話はフォーラムの投稿を Jev で判定するに書きました。この記事では、Jev に画像を渡す方法、公開しない画像の決め方、判定を通るまで画像を見せない作り、判定できなかった画像も受け付けない理由を書きます。

Jev に画像を渡す

画像も、文と同じ typesafe/jev に Cloudflare AI Gateway 経由で渡します。Jev の画像の入力は試験的な機能で、TypeSafe-Danger-Zone-Images のヘッダーを付けて有効にします。

画像は、状況を説明する文(state)の中に {{forum_image}} のような目印を書き、assets で中身を渡します。

body: JSON.stringify({
  model: "typesafe/jev",
  input: {
    state: `A user uploaded this image to the community forum of a metronome app, where musicians talk about practice:\n{{forum_image}}`,
    assets: {
      "{{forum_image}}": { data_base64: base64, format: "jpeg" },
    },
    questions: FORUM_IMAGE_QUESTIONS,
  },
}),

画像は文と同じく入力のトークンで数えられ、1 枚 1,000〜1,600 トークン、$0.00007 ほどです。

問いは 1 つ、境目は文より厳しく

問いは 1 つです。選択肢には、ふつうの画像(ok)のほかに、公開しない理由を 4 種類用意しました。

export const FORUM_IMAGE_QUESTIONS = {
  safety: {
    type: "choice",
    instructions: "Is this image acceptable to show in the forum?",
    criteria: {
      ok: "An ordinary image: a photo, a screenshot of an app or a website, sheet music, instruments, a diagram, a chart, or a harmless meme.",
      sexual: "Nudity or sexual content.",
      violent: "Graphic violence, gore, injury or self-harm.",
      hateful: "Hate symbols, harassment of a person, or text that insults a group of people.",
      spam: "An advertisement, a QR code, a phone number or contact information for selling or promoting something, or a scam.",
    },
  },
} as const;

ok には、楽譜・楽器・アプリの画面・グラフなど、このフォーラムに貼られそうな画像を並べました。スパム(spam)には、広告のほかに QR コードや売り込みのための連絡先を入れています。投稿の文だけを判定していると、宣伝を画像にして貼られたときに、すり抜けられてしまうためです。

Jev は選択肢ごとの確率を返します。公開しない 4 種類のうち、いちばん確率の高いものが 0.7 以上なら、その画像を断ります。文では、この境目(しきい値)を 0.9 にしています。画像は境目を低くして、文より断りやすくしました。画像は文より被害が大きいからです。性的な画像や暴力的な画像は、いったん公開されると見た人の目に残ります。

画像をアップロードした本文の欄。1 はフォーラムに載せられない画像と判定されたというエラー
判定で断られた画像
  1. 画像が判定で断られたときのエラー

Jev が受け付ける大きさに、ブラウザで縮める

Jev が受け付ける画像は、PNG か JPEG で、縦横が 280〜1568px のものだけです。範囲の外の画像を渡すと、Jev は縮めてはくれず、受け付けません。

そこで、アップロードする前にブラウザで画像を作り直し、長辺を 1568px までの JPEG にしています。小さい画像を大きくはしないので、もとから小さい画像や、縮めると短辺が 280px に届かない細長い画像は、その場で断ります。作り直すので、撮った場所などの EXIF も消えます。透明な部分は白で塗っています。JPEG は透明を持てず、そのままだと黒くなるためです。

const { width, height } = fitForumImageSize(bitmap.width, bitmap.height);
if (Math.min(width, height) < FORUM_IMAGE_MIN_SIDE) {
  return { error: "too-small" };
}
context.fillStyle = "#ffffff";
context.fillRect(0, 0, width, height);
context.drawImage(bitmap, 0, 0, width, height);
本文の欄。1 は画像が小さすぎて縦横とも 280px 以上にしてほしいというエラー
小さすぎる画像はアップロードする前に断る
  1. 縦横の短いほうが 280px に届かないときのエラー

ブラウザを通さずに、細工したファイルをアップロードされることもあります。そのため、サーバーでも受け取ったファイルを読み、JPEG であることと、縦横がブラウザから伝えられたとおりで範囲の中にあることを確かめてから、Jev に渡します。

判定を通るまで公開しない

貼った画像は、本文の中に Markdown の画像の書き方(![説明](URL))で入ります。判定するのは、画像を貼ってアップロードした時点です。アップロードしているあいだは、本文の画像を入れた位置に「アップロード中」の仮の行が入り、判定を通ると画像の URL に置き換わります。

本文の欄。1 はアップロードしているあいだに入る「アップロード中」の行、2 は「画像を追加」のボタン
画像をアップロードしているところ
  1. アップロードしているあいだ、本文に入る仮の行。判定を通ると画像の URL に置き換わる
  2. 「画像を追加」のボタン。ドラッグ&ドロップと貼り付けでもアップロードできる

アップロードした画像は、まず公開しない受け取り用の置き場所(Cloudflare R2 のバケット)に入れます。判定を通ったものだけを、サーバーが公開用の置き場所に移します。通らなかった画像と判定できなかった画像は、受け取り用から消します。受け取り用の置き場所は公開していないので、判定の結果が出るまで、画像はどこからも見えません。

判定は 1 枚につき 1 回だけです。サーバーは、画像ごとにデータベースへ記録(台帳)を作り、「まだ確かめていない」「確かめている」などの状態を持たせています。Jev を呼ぶのは、状態を「まだ確かめていない」から「確かめている」に書き換えられたときだけです。同じ画像の判定を何度頼まれても、2 回目からは書き換えられないので、判定の費用は 1 回分で済みます。1 つの投稿に貼れる 4 枚とは別に、アップロードできる数にも 1 人 1 時間 30 枚の上限を付けています。

本文の欄のプレビュー。1 は判定を通って公開用の置き場所から出した画像
判定を通った画像のプレビュー
  1. 判定を通って公開用の置き場所に移った画像

本文に貼れるのは、自分がアップロードして判定を通った画像だけです。投稿するときに本文から画像の URL をすべて取り出して台帳と比べ、判定を通っていない画像や他人の画像は貼れないようにしています。

判定できなかった画像は受け付けない

通信エラーや、形の違う応答が返ってきたときは、判定できません。文の判定では、このとき投稿を止めずに通し、あとで見直せるよう、判定していないことを示す unchecked を投稿に残しています。画像はその逆で、判定できなかったら受け付けません。理由は 3 つあります。

1 つめは、画像の入力が試験的な機能で、失敗しうることです。2 つめは、先に書いたとおり、画像は公開してから消しても遅いことです。3 つめは、アップロードする人の手間が小さいことです。画像は本文とは別に、貼った時点で判定するので、断られても書いた文は消えません。時間をおいて同じ画像をアップロードし直せば済みます。

文
画像
判定する時点
投稿・編集を保存する前
貼った時点(アップロードした直後)
断る境目
0.9(フォーラムと関係ない話題は 0.95)
0.7
Jev の応答を待つ時間
5 秒
10 秒
判定できなかったとき
通して unchecked を残す
受け付けない

決まった画像で、境目が合っているかを見る

Cloudflare 経由では Jev の版を固定できず、中身は新しい版に入れ替わります。そこで、決まった画像を Jev に判定させるスクリプトを用意し、ときどき境目が合っているかを見ています。

画像は 10 枚あります。通すべき画像は、アプリの画面・五線と音符・練習の記録のグラフなどの 6 枚で、断るべき画像は、セールの広告・副業の勧誘と連絡先・QR コードと勧誘・集団を罵る文字の 4 枚です。通すか断るかが合っていれば合格と数えて、今は 10 枚中 10 枚が合格です。

この画像は、アプリの画面の 2 枚を除いてファイルを置かず、HTML を Playwright で表示してその場で JPEG にしています。性的な画像と暴力的な画像は、集めて手元に置くこと自体がよくないので試していません。その 2 つの判定は Jev に任せています。

この記事を共有

X でポストはてなブックマーク