OpenAI個人情報保護モデルで3つのアプリを構築

モデルの特徴と性能

15億パラメータ、活性50Mの軽量設計
Apache 2.0の寛容ライセンス
128Kトークンの長文一括処理
PII検出ベンチマーク最高精度達成

3種のデモアプリ構成

PDF等の個人情報を自動強調表示
画像内の個人情報を黒塗り処理
貼り付けテキストの秘匿共有機能
gradio.Serverで統一的に構築
詳細を読む

OpenAIが公開した個人情報保護モデル「Privacy Filter」を活用し、Hugging Face開発者3名が実用的なWebアプリ3本を構築しました。Privacy Filterは15億パラメータのモデルで、活性パラメータは5000万、Apache 2.0ライセンスで提供されています。128Kトークンのコンテキストに対応し、PII検出ベンチマークで最高精度を達成しています。

1つ目の「Document Privacy Explorer」は、PDFやDOCXファイルをアップロードすると、個人名・メールアドレス・電話番号などの個人情報を自動検出してカテゴリ別にハイライト表示するアプリです。128Kコンテキストを活かし、文書全体を一括処理するためチャンク分割が不要です。

2つ目の「Image Anonymizer」は、スクリーンショットや画像内の個人情報を黒塗りで自動秘匿するツールです。Tesseract OCRで文字領域を抽出した後にPrivacy Filterで検出し、ピクセル座標の矩形として返します。ブラウザ上でバーの表示切替やドラッグ移動、手動追加も可能です。

3つ目の「SmartRedact Paste」は、テキストを貼り付けると秘匿済みの公開URLと、原文を確認できるトークン付き非公開URLの2つを生成するプライバシー対応ペーストビンです。多言語テキストにも対応しています。

3つのアプリはすべてgradio.Server上に構築されています。モデル推論は@server.apiデコレータでGradioのキューに載せ、ZeroGPU割り当てやプログレス通知を活用します。静的ページの配信にはFastAPIのルートを使い、モデル呼び出しとUI提供を明確に分離する設計パターンが共通しています。

\n[\n](#2-image-anonymizer)\n2. Image Anonymizer\nTry it at [ysharma/OPF-Image-Anonymizer](https://huggingface.co/spaces/ysharma/OPF-Image-Anonymizer).\nUser 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.\nWhat 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.\nWhat gr.Server does here.\ngr.ImageEditor\nsupports 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 \nfrontend. gr.Server\nhands back pixel rectangles from one queued endpoint and lets the canvas own everything else:@server.api(name=\"anonymize_screenshot\")\ndef anonymize_screenshot(image: FileData) -> dict:\nimg = Image.open(image[\"path\"]).convert(\"RGB\")\nfull_text, char_to_box = ocr_image(img) # per-word boxes + char map\nspans = run_privacy_filter(full_text)\nboxes = spans_to_pixel_boxes(spans, char_to_box)\nreturn {\n\"image_data_url\": pil_to_base64(img),\n\"width\": img.width,\n\"height\": img.height,\n\"boxes\": boxes, # [{x, y, w, h, label, text}, ...]\n}\nThe frontend invokes it with client.predict(\"/anonymize_screenshot\", { image: handle_file(file) })\n, 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.\n[\n](#3-smartredact-paste)\n3. SmartRedact Paste\nTry it at [ysharma/OPF-SmartRedact-Paste](https://huggingface.co/spaces/ysharma/OPF-SmartRedact-Paste).\nUser 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 \n, \n, \nplaceholders, following the redaction convention from the [official blog examples](https://openai.com/index/introducing-openai-privacy-filter/#:~:text=coherent%20masking%20boundaries.-,Example%20input%20text,-Subject%3A%20Q2%20Planning). The private one is gated by a token you keep and shows the original with spans highlighted.\nWhat Privacy Filter does here. Swap each detected span with a \nplaceholder 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.\nWhat 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.\ngr.Server\nworks here because it's a FastAPI app underneath — which is also why @server.api\nand plain @server.get\ncan sit side by side in the same process. Note: this can also be built with gr.Blocks()\nby [mounting custom routes with FastAPI](https://www.gradio.app/docs/gradio/mount_gradio_app):\n# Model call → queued endpoint. Hit from the browser via\n# client.predict(\"/create_paste\", { text, ttl }).\[email protected](name=\"create_paste\")\ndef create_paste(text: str, ttl: str = \"never\") -> dict:\nsource_text, spans = run_privacy_filter(text)\nredacted = redact(source_text, spans) # placeholders\npid, reveal_token = secrets.token_urlsafe(6), secrets.token_urlsafe(22)\nPASTES[pid] = Paste(pid, reveal_token, source_text, redacted, spans,\nexpires_at=_ttl(ttl)) # see app.py\nreturn {\n\"view_path\": f\"/view/{pid}\",\n\"reveal_path\": f\"/view/{pid}?token={reveal_token}\",\n}\n# View page → plain FastAPI GET. No model, no queue needed, and we\n# actually want the bespoke URL shape `/view/{pid}?token=...` that a\n# queued endpoint couldn't give us.\[email protected](\"/view/{pid}\", response_class=HTMLResponse)\nasync def view_paste(pid: str, token: str | None = None):\np = _store_get(pid) # see app.py for store\nif p is None:\nreturn HTMLResponse(_not_found(), status_code=404)\nrevealed = bool(token) and secrets.compare_digest(token, p.reveal_token)\nreturn HTMLResponse(_render_view(p, revealed))\nA 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.\n[\n](#what-gradioserver-provides)\nWhat gradio.Server\nprovides\nThe split across all three apps is the same — anything that touches the model goes through @server.api\n, everything else stays on plain FastAPI routes:\n| App | Queued compute (@server.api ) |\nPlain FastAPI routes |\n|---|---|---|\n| Document Privacy Explorer | analyze_document — extract, detect, stats |\nGET / serves the custom reader view |\n| Image Anonymizer | anonymize_screenshot — OCR, detect, spans → pixel boxes |\nGET / + GET /examples/* serve the canvas UI and preloaded examples |\n| SmartRedact Paste | create_paste — detect, redact, mint IDs |\nGET / compose page, GET /view/{pid}?token=... public + token-gated views, GET /api/paste/{pid} JSON lookup |\[email protected]\ngives you Gradio's queue (serialized requests, correct @spaces.GPU\ncomposition on ZeroGPU, progress events) and it's what the browser hits through [ @gradio/client](https://www.gradio.app/guides/getting-started-with-the-js-client). The same endpoint is also what\ngradio_client\nusers hit from Python — one function, two SDKs, no duplicated code. Plain @server.get\n/@server.post\nare reserved for the static surfaces: HTML pages, file lookups, cheap dict reads. That's the rule of thumb from the [gradio.Server intro post](https://huggingface.co/blog/introducing-gradio-server), and it's what makes these three apps feel consistent even though their UIs are very different.\n[\n](#try-them)\nTry them\nDrop 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.\n[\n](#recommended-reading)\nRecommended reading\n- OpenAI's release post:\n[Introducing OpenAI Privacy Filter](https://openai.com/index/introducing-openai-privacy-filter/) - Model card:\n[openai/privacy-filter on Hugging Face](https://huggingface.co/openai/privacy-filter) [Redaction examples and taxonomy on Model card](https://cdn.openai.com/pdf/c66281ed-b638-456a-8ce1-97e9f5264a90/OpenAI-Privacy-Filter-Model-Card.pdf)", "image": "https://huggingface.co/blog/assets/openai-privacy-filter-web-apps/thumbnail.png", "source": "https://huggingface.co/blog/openai-privacy-filter-web-apps", "source-hostname": "Hugging Face", "tags": ""}}], "cluster_info": {"representative_id": "article_20260427_e548e0", "reason_brief": "Single article on this topic.", "title_synthesis": "Guide to building scalable web apps with OpenAI's Privacy Filter"}}