OpenAIのプライバシーフィルターを用いたスケーラブルなWebアプリケーション構築方法
OpenAI が Hugging Face Hub に公開したオープンソースのプライバシーフィルターは、128k コンテキスト内で個人特定情報(PII)を8カテゴリ検出可能であり、スケーラブルな Web アプリ開発におけるデータ保護の実用的な解決策を提供する。
キーポイント
高性能な PII 検出機能
単一のフォワードパスで 128k のコンテキスト長を処理し、8 つの異なるカテゴリにわたって個人特定情報(PII)をラベル付けする。
オープンソース化と Hugging Face Hub での公開
OpenAI が開発したこのツールがオープンソースとして Hugging Face Hub にリリースされ、誰でも利用可能になった。
実用的な Web アプリケーションの構築事例
記事では、このフィルターを活用してスケーラブルな Web アプリを構築するための具体的な 3 つのユースケースとアプローチが紹介されている。
128k コンテキストによる単一パス処理
ファイル全体をチャンキングや結合なしで128kコンテキストのフォワードパスで処理し、検出されたスパンのオフセットがレンダリングされたテキストと正確に一致します。
クライアントサイドでのインタラクション最適化
カテゴリフィルタはCSSクラスを切り替えることでモデル再実行を防ぎ、要約ダッシュボードもページ再描画を強制しない設計により、通常の文書のような読み体験を実現します。
gr.Server を用いたカスタムビューの実装
Gradio Blocks のコンポジションよりも手動でHTMLを記述する方が容易なため、gr.Server を使用してリーダービューを単一のHTMLファイルとして提供し、モデルはキューイングされたエンドポイント経由で暴露しています。
Gradio API デコレータの役割
@server.api を使用することで、ハンドラを Gradio のキューに接続し、同時アップロードのシリアライズや GPU リソースの適切な構成が可能になる。
影響分析・編集コメントを表示
影響分析
このニュースは、大規模言語モデル(LLM)を扱うアプリケーション開発において、データのセキュリティとコンプライアンス遵守を容易にする重要なツールが登場したことを示しています。特に、長文のコンテキストを効率的に処理できる点は、実務レベルでのスケーラブルなシステム構築において即座に活用可能な価値を持ちます。
編集コメント
OpenAI が自社の技術をオープンソースとして公開し、コミュニティが即座に活用できる形での提供を行った点は、業界全体のセキュリティ標準向上に寄与する重要な動きです。開発者はこのツールを活用することで、複雑なプライバシー対策を最小限のコードで実装できるようになります。
OpenAIは先週、Hub上でPrivacy Filterをリリースしました。これはオープンソースの個人識別情報(PII)検出器で、128kコンテキストに対する単一フォワードパスで、8つのカテゴリにわたるテキストにラベルを付けます。モデルカード。私たちはこれを使って数時間開発し、その機能の異なる側面をそれぞれ示す3つのアプリに辿り着きました。
- ドキュメントプライバシーエクスプローラー:PDFまたはDOCXをドロップすると、PIIの各スパンがその場で強調表示された状態でドキュメントを読み取ることができます。
- 画像匿名化ツール:画像をアップロードすると、名前、メールアドレス、口座番号の上に黒い塗りつぶしバーで処理された画像が返されます。また、キャンバス上で編集可能なので、ダウンロードする前に独自の注釈を追加することもできます。
- SmartRedactペースト:機密テキストを貼り付け、処理されたバージョンを提供する公開URLを共有し、自分専用のプライベートな表示リンクを保持します。
これら3つはすべてgradio.Server上で構築されており、カスタムHTML/JSフロントエンドをGradioのキューイング、ZeroGPU割り当て、およびgradio_client SDKと組み合わせることができます。これらのアプリすべてにおいて、gradio.Serverは同じバックエンドの役割を果たしており、この一貫性がまさにその強力な理由です。
モデル
Privacy Filter は 15 億パラメータのモデルで、アクティブなパラメータ数は 5,000 万です。Apache 2.0 ライセンスの下で自由に利用可能です。PII(個人識別情報)のカテゴリには、private_person(個人)、private_address(住所)、private_email(メール)、private_phone(電話)、private_url(URL)、private_date(日付)、account_number(口座番号)、secret(秘密情報)が含まれます。コンテキスト長は 128,000 トークンです。PII-Masking-300k ベンチマークにおいて、最先端の性能を達成しています。詳細な数値と方法論は公式リリースブログをご覧ください。
1. ドキュメントプライバシーエクスプローラー
ysharma/OPF-Document-PII-Explorer でお試しください。
ユーザーの課題。 PII が大量に含まれる文書(契約書、履歴書、エクスポートされたチャットログなど)を読み取りたい場合、検出された各スパンがカテゴリ別にハイライトされ、サイドバーにフィルターがあり、上部に要約ダッシュボードが表示されることを望みます。読みやすさは通常の文書と変わらないものであり、フォームのようなものではないことが求められます。
ここで Privacy Filter が行うこと。 全文が単一の 128k コンテキストの順伝播処理を通じて処理されるため、チャンキング(分割)も不要で、結合も必要なく、スパンのオフセットはレンダリングされたテキストと直接一致します。BIOES デコーディングにより、長い曖昧な領域においてもスパンの境界が明確に保たれます。
ここで gr.Server が行っていること。 Blocks で gr.HighlightedText とサイドバーを接続すれば、これと同様の機能を実現できますが、私たちが目指した読書体験(セリフ体の本文、モデルを再実行せずにクライアント側で CSS クラスを切り替えるカテゴリフィルター、ページ再レンダリングを強制しないサマリーダッシュボード)を手書きで実装する方が、コンポーネントを組み立てるよりも容易でした。gr.Server を使用することで、読者向けビューを単一の HTML ファイルとして提供し、1 つのキュー付きエンドポイントを通じてモデルへのアクセスを公開しています。
import gradio as gr
from fastapi.responses import HTMLResponse
from gradio.data_classes import FileData
server = gr.Server()
@server.get("/", response_class=HTMLResponse)
async def homepage():
return FRONTEND_HTML # 読者向けビュー; app.py を参照
@server.api(name="analyze_document")
def analyze_document(file: FileData) -> dict:
text = extract_text(file["path"]) # PyMuPDF / python-docx
source_text, spans = run_privacy_filter(text) # 128k の単一パス
return {
"text": source_text,
"spans": spans, # [{start, end, label}, ...]
"stats": compute_stats(source_text, spans),
}
@server.api(name="analyze_document") というデコレータに注目してください。これは単なる @server.post ではありません。この部分がハンドラを Gradio のキューに接続し、並列アップロードを直列化し、@spaces.GPU が ZeroGPU 上で正しく合成され、重複コードなしでブラウザと gradio_client の両方から同じエンドポイントにアクセス可能になります。ブラウザは Gradio JS クライアントを使用してこれを呼び出します:
<script type="module">
import { Client, handle_file } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js";
const client = await Client.connect(window.location.origin);
async function uploadFile(file) {
const result = await client.predict("/analyze_document", { file: handle_file(file) });
renderResults(result.data[0]); // { text, spans, stats }
}
</script>
2. 画像匿名化ツール
ysharma/OPF-Image-Anonymizer でお試しください。
ユーザーの課題。 PII(個人識別情報)の上に黒いバーを配置した画像やスクリーンショット(Slack のスレッド、領収書、Stripe ダッシュボードなど)を共有したい場合、バーの表示・非表示を切り替えたり、ドラッグして位置を調整したり、モデルが見逃したものを手動で描画したりしてから結果をエクスポートしたいとします。
ここで Privacy Filter が行う処理。 Tesseract が OCR を実行し、単語ごとの境界ボックスを返します。バックエンドは文字オフセットからボックスへのマッピングを使用して完全なテキストを再構築し、その後 Privacy Filter を全文に対して一度実行します。検出された文字範囲は単語マッピングに対して照合され、行ごとにピクセル矩形に結合されます。
ここでgr.Serverが行っていること。 gr.ImageEditorは階層付き注釈をサポートしており、画像のモザイク処理(redaction)の妥当な出発点となります。私たちが望んでいたワークフロー(カテゴリごとのメタデータ、カテゴリ内のすべてのバーを一度に切り替える機能、サーバーとの往復通信なしでクライアントサイドで自然解像度のPNGエクスポートを行う機能)は、カスタム<canvas>フロントエンド上に構築する方がより簡潔でした。gr.Serverは1つのキューイング済みエンドポイントからピクセル矩形を返し、キャンバスが他のすべてを管理できるようにしています:
@server.api(name="anonymize_screenshot")
def anonymize_screenshot(image: FileData) -> dict:
img = Image.open(image["path"]).convert("RGB")
full_text, char_to_box = ocr_image(img) # 単語ごとのボックスと文字マップ
spans = run_privacy_filter(full_text)
boxes = spans_to_pixel_boxes(spans, char_to_box)
return {
"image_data_url": pil_to_base64(img),
"width": img.width,
"height": img.height,
"boxes": boxes, # [{x, y, w, h, label, text}, ...]
}
フロントエンドは client.predict("/anonymize_screenshot", { image: handle_file(file) }) を用いてこれを呼び出します。これは上記と同じパターンです。切り替え、ドラッグ操作、新しいバーの描画、PNGエクスポートはすべてブラウザ内で発生し、編集内容はサーバーへの往復通信を行いません。
3. SmartRedact Paste
ysharma/OPF-SmartRedact-Paste でお試しください。
ユーザーの課題。 共有前に機密情報を伏字にするPastebin(テキスト共有サービス)が欲しい。ログ行、メールアドレス、サポートチケットなどを貼り付けると、2つのURLが返ってくる。公開用URLは、公式ブログの例で示された伏字の規約に従い、<PRIVATE_PERSON>、<PRIVATE_EMAIL>、<ACCOUNT_NUMBER>というプレースホルダーを使用して伏字処理されたバージョンを提供する。プライベート用URLは保持しているトークンで保護され、元のテキストが表示されるが、特定の範囲(span)が強調表示される。
ここでPrivacy Filterが行う処理。 検出された各範囲を、保存されたペースト内の<CATEGORY>プレースホルダーに置き換える。これこそが伏字処理の全工程である。多言語テキスト(モデルカードの例に含まれるスペイン語、フランス語、中国語、ヒンディー語など)は、同じ呼び出しを通じて処理され、変更はない。
ここでgr.Serverが行う処理。 このアプリには、同じペーストIDに対して公開用とトークン保護用の2つの異なるGETルートが必要であり、URLの形状が重要になる。これは「表示用URL」を保持するものだからである。gr.Serverは、その内部がFastAPIアプリであるため(そのため@server.apiと通常の@server.getを同じプロセス内で並置できる)、ここで機能する。なお、FastAPIを使用してカスタムルートをマウントすることで、gr.Blocks()でもこれを構築可能である:
モデル呼び出し → キュー化されたエンドポイント。ブラウザからのアクセスは
client.predict("/create_paste", { text, ttl }) を介して行われます。
@server.api(name="create_paste")
def create_paste(text: str, ttl: str = "never") -> dict:
source_text, spans = run_privacy_filter(text)
redacted = redact(source_text, spans) # <CATEGORY> プレースホルダー
pid, reveal_token = secrets.token_urlsafe(6), secrets.token_urlsafe(22)
PASTES[pid] = Paste(pid, reveal_token, source_text, redacted, spans,
expires_at=_ttl(ttl)) # app.py を参照
return {
"view_path": f"/view/{pid}",
"reveal_path": f"/view/{pid}?token={reveal_token}",
}
表示ページ → 通常の FastAPI GET リクエスト。モデル呼び出しもキュー処理も不要で、
キュー化されたエンドポイントでは提供できない独自な URL 形式 /view/{pid}?token=...
を実際に取得できます。
@server.get("/view/{pid}", response_class=HTMLResponse)
async def view_paste(pid: str, token: str | None = None):
p = _store_get(pid) # ストアの実装は app.py を参照
if p is None:
return HTMLResponse(_not_found(), status_code=404)
revealed = bool(token) and secrets.compare_digest(token, p.reveal_token)
return HTMLResponse(_render_view(p, revealed))
期限切れの投稿(paste)を削除するデーモンスレッドが、30 秒ごとに実行されます。ストレージを含むこのサービス全体は、すべての処理を単一のプロセスで完結させるため、約 200 行のアプリケーションコードで構成されています。
gradio.Server が提供する機能
3 つのアプリすべてで分割は同じです。モデルにアクセスするものはすべて @server.api を経由し、それ以外は通常の FastAPI ルートに残ります:
アプリ
キュー処理された計算 (@server.api)
通常の FastAPI ルート
Document Privacy Explorer
analyze_document — 抽出、検出、統計
GET / はカスタムリーダービューを提供します
Image Anonymizer
anonymize_screenshot — OCR、検出、スパン → ピクセルボックス
GET / および GET /examples/* はキャンバス UI とプリロードされた例を提供します
SmartRedact Paste
create_paste — 検出、赤塗り、ID の発行
GET / はページを構成し、GET /view/{pid}?token=... は公開およびトークンゲート付きビューを提供し、GET /api/paste/{pid} は JSON ルックアップを提供します
@server.api により、Gradio のキュー(直列化されたリクエスト、ZeroGPU 上の正しい @spaces.GPU コンポジション、進行状況イベント)が提供され、ブラウザは @gradio/client を通じてこれにアクセスします。同じエンドポイントは、Python から gradio_client ユーザーがアクセスするものでもあります。1 つの関数、2 つの SDK、重複コードなしです。通常の @server.get/@server.post は静的なサーフェス(HTML ページ、ファイルルックアップ、安価な辞書読み取り)に予約されています。これは gradio.Server 入門投稿 の経験則であり、UI が非常に異なっていても、これら 3 つのアプリが一貫性を持って感じられる理由です。
試してみる
- Document Privacy Explorer
- Image Anonymizer
- SmartRedact Paste
履歴書、Slack のスレッドのスクリーンショット、トークンを含むログ行などを投入してください。実際に気にしているテキストに対して、Privacy Filter が何を(そして稀に何を)検出するかを見てみるのが楽しい部分です。
推奨読書
- OpenAI のリリース記事:OpenAI Privacy Filter の紹介
- Hugging Face 上のモデルカード:openai/privacy-filter
- モデルカードにあるリダクション(匿名化)の例と分類体系
原文を表示
OpenAI released Privacy Filter on the Hub this week: an open-source personally-identifiable information (PII) detector that labels text across eight categories in a single forward pass over a 128k context. Model card. We spent a few hours building with it and landed on three apps that each reveals a different slice of what it can do.
- Document Privacy Explorer: drop in a PDF or DOCX, read the document back with every PII span highlighted in place.
- Image Anonymizer: upload an image, get it back with redacted black bars over names, emails, and account numbers. The image is also editable on a canvas so you can make your own annotations before downloading.
- SmartRedact Paste: paste sensitive text, share a public URL that serves the redacted version, keep a private reveal link for yourself.
All three are built on gradio.Server, which lets you pair custom HTML/JS frontends with Gradio's queueing, ZeroGPU allocation, and gradio_client SDK. In all these apps, gradio.Server plays the same backend role, and that consistency is exactly what makes it really powerful.
The model
Privacy Filter is a 1.5B-parameter model with 50M active parameters, permissively licensed under Apache 2.0. PII categories are private_person, private_address, private_email, private_phone, private_url, private_date, account_number, secret. Context is 128,000 tokens. Achieves state-of-the-art performance on the PII-Masking-300k benchmark. Full numbers and methodology are in the official release blog.
1. Document Privacy Explorer
Try it at ysharma/OPF-Document-PII-Explorer.
User problem. You want to read a PII-heavy document (a contract, a resume, an exported chat log) with every detected span highlighted by category, a filter in the sidebar, and a summary dashboard up top. The reading experience should feel like a normal document, not a form.
What Privacy Filter does here. The whole file goes through in a single 128k-context forward pass, so there's no chunking, no stitching, and span offsets line up directly with the rendered text. BIOES decoding keeps span boundaries clean through long ambiguous runs.
What gr.Server does here. You could wire this up in Blocks with gr.HighlightedText and a sidebar, and it would work. The reading experience we wanted (serif body, category filters that toggle CSS classes client-side instead of re-running the model, a summary dashboard that doesn't force a page re-render) was easier to hand-author than to compose. gr.Server lets us serve the reader view as a single HTML file and expose the model behind one queued endpoint:
import gradio as gr
from fastapi.responses import HTMLResponse
from gradio.data_classes import FileData
server = gr.Server()
@server.get("/", response_class=HTMLResponse)
async def homepage():
return FRONTEND_HTML # reader view; see app.py
@server.api(name="analyze_document")
def analyze_document(file: FileData) -> dict:
text = extract_text(file["path"]) # PyMuPDF / python-docx
source_text, spans = run_privacy_filter(text) # single 128k pass
return {
"text": source_text,
"spans": spans, # [{start, end, label}, ...]
"stats": compute_stats(source_text, spans),
}
Note the decorator: @server.api(name="analyze_document"), not a plain @server.post. That's the piece that plugs the handler into Gradio's queue, so concurrent uploads are serialized, @spaces.GPU composes correctly on ZeroGPU, and the same endpoint is reachable from both the browser and gradio_client with no duplicated code. The browser calls it with the Gradio JS client:
<script type="module">
import { Client, handle_file } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js";
const client = await Client.connect(window.location.origin);
async function uploadFile(file) {
const result = await client.predict("/analyze_document", { file: handle_file(file) });
renderResults(result.data[0]); // { text, spans, stats }
}
</script>
2. Image Anonymizer
Try it at ysharma/OPF-Image-Anonymizer.
User problem. You want to share an image or any screenshot (a Slack thread, a receipt, a Stripe dashboard) with black bars over the PII. You want to toggle bars on and off, drag them to reposition, or draw one by hand for anything the model missed, then export the result.
What Privacy Filter does here. Tesseract runs OCR and returns per-word bounding boxes. The backend reconstructs the full text with a char-offset to box map, then runs Privacy Filter once over the whole text. Detected character spans are looked up against the word map and joined into pixel rectangles per line.
What gr.Server does here. gr.ImageEditor supports layered annotation and is a reasonable starting point for image redaction. The workflow we wanted (per-bar category metadata, toggle all bars in a category at once, client-side PNG export at natural resolution with no server round-trip) was cleaner to build on a custom <canvas> frontend. gr.Server hands back pixel rectangles from one queued endpoint and lets the canvas own everything else:
@server.api(name="anonymize_screenshot")
def anonymize_screenshot(image: FileData) -> dict:
img = Image.open(image["path"]).convert("RGB")
full_text, char_to_box = ocr_image(img) # per-word boxes + char map
spans = run_privacy_filter(full_text)
boxes = spans_to_pixel_boxes(spans, char_to_box)
return {
"image_data_url": pil_to_base64(img),
"width": img.width,
"height": img.height,
"boxes": boxes, # [{x, y, w, h, label, text}, ...]
}
The frontend invokes it with client.predict("/anonymize_screenshot", { image: handle_file(file) }), the same pattern as above. Toggles, drags, new-bar drawing, and PNG export all happen in the browser; edits never round-trip to the server.
3. SmartRedact Paste
Try it at ysharma/OPF-SmartRedact-Paste.
User problem. You want a pastebin that redacts before sharing. You paste a log line, an email, a support ticket. You get two URLs back. The public one serves the redacted version with <PRIVATE_PERSON>, <PRIVATE_EMAIL>, <ACCOUNT_NUMBER> placeholders, following the redaction convention from the official blog examples. The private one is gated by a token you keep and shows the original with spans highlighted.
What Privacy Filter does here. Swap each detected span with a <CATEGORY> placeholder on the stored paste. That's the entire redaction step. Multilingual text (Spanish, French, Chinese, Hindi, and others in the model-card examples) routes through the same call with no change.
What gr.Server does here. This app needs two distinct GET routes for the same paste ID, one public and one token-gated, and the URL shape matters because the reveal URL is the thing you keep. gr.Server works here because it's a FastAPI app underneath — which is also why @server.api and plain @server.get can sit side by side in the same process. Note: this can also be built with gr.Blocks() by mounting custom routes with FastAPI :
# Model call → queued endpoint. Hit from the browser via
# client.predict("/create_paste", { text, ttl }).
@server.api(name="create_paste")
def create_paste(text: str, ttl: str = "never") -> dict:
source_text, spans = run_privacy_filter(text)
redacted = redact(source_text, spans) # <CATEGORY> placeholders
pid, reveal_token = secrets.token_urlsafe(6), secrets.token_urlsafe(22)
PASTES[pid] = Paste(pid, reveal_token, source_text, redacted, spans,
expires_at=_ttl(ttl)) # see app.py
return {
"view_path": f"/view/{pid}",
"reveal_path": f"/view/{pid}?token={reveal_token}",
}
# View page → plain FastAPI GET. No model, no queue needed, and we
# actually want the bespoke URL shape `/view/{pid}?token=...` that a
# queued endpoint couldn't give us.
@server.get("/view/{pid}", response_class=HTMLResponse)
async def view_paste(pid: str, token: str | None = None):
p = _store_get(pid) # see app.py for store
if p is None:
return HTMLResponse(_not_found(), status_code=404)
revealed = bool(token) and secrets.compare_digest(token, p.reveal_token)
return HTMLResponse(_render_view(p, revealed))
A daemon thread evicts expired pastes every 30 seconds. The whole service, including storage, is about 200 lines of application code because everything lives in one process.
What gradio.Server provides
The split across all three apps is the same — anything that touches the model goes through @server.api, everything else stays on plain FastAPI routes:
App
Queued compute (@server.api)
Plain FastAPI routes
Document Privacy Explorer
analyze_document — extract, detect, stats
GET / serves the custom reader view
Image Anonymizer
anonymize_screenshot — OCR, detect, spans → pixel boxes
GET / + GET /examples/* serve the canvas UI and preloaded examples
SmartRedact Paste
create_paste — detect, redact, mint IDs
GET / compose page, GET /view/{pid}?token=... public + token-gated views, GET /api/paste/{pid} JSON lookup
@server.api gives you Gradio's queue (serialized requests, correct @spaces.GPU composition on ZeroGPU, progress events) and it's what the browser hits through @gradio/client. The same endpoint is also what gradio_client users hit from Python — one function, two SDKs, no duplicated code. Plain @server.get/@server.post are reserved for the static surfaces: HTML pages, file lookups, cheap dict reads. That's the rule of thumb from the gradio.Server intro post, and it's what makes these three apps feel consistent even though their UIs are very different.
Try them
- Document Privacy Explorer
- Image Anonymizer
- SmartRedact Paste
Drop in a resume, a screenshot of a Slack thread, a log line with a token in it. The fun part is seeing what Privacy Filter catches (and occasionally misses) on text you actually care about.
Recommended reading
- OpenAI's release post: Introducing OpenAI Privacy Filter
- Model card: openai/privacy-filter on Hugging Face
- Redaction examples and taxonomy on Model card
関連記事
今日のまとめ
AI日報で今日の重要ニュースをまとめ読み