如何使用 OpenAI 的 Privacy Filter 构建可扩展 Web 应用
How to build scalable web apps with OpenAI's Privacy Filter
OpenAI 在 Hugging Face Hub 发布开源 Privacy Filter,用于 128k context 内检测八类 PII。作者基于 gradio.Server 构建 Document Privacy Explorer、Image Anonymizer 和 SmartRedact Paste,分别处理文档高亮、OCR 图片遮盖和 paste 脱敏分享,并通过 @server.api 接入 Gradio queue、ZeroGPU 与客户端 SDK。
](https://huggingface.co/ysharma)
OpenAI 本周在 Hub 上发布了 Privacy Filter:一个开源的 personally-identifiable information(PII,个人身份信息)检测器,可在一次 128k context 的 forward pass 中对文本进行八类标注。Model card。我们花了几个小时基于它构建应用,最后做出了三个 app,每个 app 展示了它能力的不同侧面。
- Document Privacy Explorer:放入 PDF 或 DOCX,读取文档时,所有 PII span 都会在原位高亮显示。
- Image Anonymizer:上传一张图片,返回的图片会用黑条遮盖姓名、邮箱和账号。图片也可以在 canvas 上编辑,因此你可以在下载前添加自己的标注。
- SmartRedact Paste:粘贴敏感文本,分享一个提供脱敏版本的公开 URL,同时为自己保留一个私密 reveal link。
这三个 app 都基于 gradio.Server 构建,它让你可以把自定义 HTML/JS frontend 与 Gradio 的 queueing、ZeroGPU allocation 和 gradio_client SDK 搭配使用。在这些 app 中,gradio.Server 扮演的是同一个 backend 角色,而这种一致性正是它强大的地方。
模型
Privacy Filter 是一个 1.5B 参数模型,其中 50M 为 active parameters,采用宽松的 Apache 2.0 许可证。PII 类别包括 private_person、private_address、private_email、private_phone、private_url、private_date、account_number、secret。Context 长度为 128,000 tokens。在 PII-Masking-300k benchmark 上达到 state-of-the-art 性能。完整数字和方法见官方发布博客。
1. Document Privacy Explorer
可在 ysharma/OPF-Document-PII-Explorer 试用。
用户问题。 你想阅读一份包含大量 PII 的文档(合同、简历、导出的聊天记录),其中每个检测到的 span 都按类别高亮显示,侧边栏有过滤器,顶部有摘要 dashboard。阅读体验应该像正常文档,而不是表单。
Privacy Filter 在这里做什么。 整个文件会在一次 128k-context forward pass 中处理,因此不需要 chunking,不需要拼接,span offset 会直接对齐到渲染后的文本。BIOES decoding 能在较长的模糊片段中保持清晰的 span 边界。
gr.Server 在这里做什么。 你可以用 Blocks、gr.HighlightedText 和侧边栏把这个功能连起来,也能正常工作。但我们想要的阅读体验(serif 正文字体、在 client-side 通过切换 CSS class 实现类别过滤而不是重新运行模型、不会强制页面重渲染的摘要 dashboard)用手写方式比组合组件更容易实现。gr.Server 让我们把阅读视图作为一个 HTML 文件提供,并把模型暴露在一个 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),
}
注意这个 decorator:@server.api(name="analyze_document"),不是普通的 @server.post。正是这个部分把 handler 接入 Gradio 的 queue,因此并发上传会被序列化,@spaces.GPU 能在 ZeroGPU 上正确组合,同一个 endpoint 也能同时由浏览器和 gradio_client 访问,无需重复代码。浏览器通过 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
可在 ysharma/OPF-Image-Anonymizer 试用。
用户问题。 你想分享一张图片或任意截图(Slack 线程、收据、Stripe dashboard),并用黑条遮盖其中的 PII。你希望可以打开或关闭黑条、拖动它们重新定位,或者为模型漏掉的内容手动画一个黑条,然后导出结果。
Privacy Filter 在这里做什么。 Tesseract 运行 OCR,并返回每个词的 bounding box。Backend 会用 char-offset 到 box 的映射重建完整文本,然后对全文运行一次 Privacy Filter。检测到的字符 span 会在词映射中查找,并按行合并成像素矩形。
gr.Server 在这里做什么。gr.ImageEditor 支持分层标注,是做图片脱敏的一个合理起点。但我们想要的 workflow(每个黑条带有类别 metadata、一次性切换某个类别的所有黑条、在 client-side 以自然分辨率导出 PNG 且无需 server round-trip)更适合用自定义 <canvas> frontend 构建。gr.Server 通过一个 queued endpoint 返回像素矩形,其余部分交给 canvas 处理:
@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}, ...]
}
Frontend 用 client.predict("/anonymize_screenshot", { image: handle_file(file) }) 调用它,模式与上面相同。切换、拖动、新建黑条绘制和 PNG 导出都在浏览器中完成;编辑永远不需要 round-trip 到 server。
3. SmartRedact Paste
可在 ysharma/OPF-SmartRedact-Paste 试用。
用户问题。 你想要一个在分享前先脱敏的 pastebin。你粘贴一行日志、一封邮件、一个支持工单。你会得到两个 URL。公开 URL 提供脱敏版本,其中使用 <PRIVATE_PERSON>、<PRIVATE_EMAIL>、<ACCOUNT_NUMBER> 这类 placeholder,遵循官方博客示例中的脱敏约定。私密 URL 由你保管的 token 保护,会显示原文并高亮 span。
Privacy Filter 在这里做什么。 在存储的 paste 中,把每个检测到的 span 替换为 <CATEGORY> placeholder。这就是完整的脱敏步骤。多语言文本(model-card 示例中的西班牙语、法语、中文、印地语等)都通过同一个调用处理,无需改动。
gr.Server 在这里做什么。 这个 app 需要为同一个 paste ID 提供两个不同的 GET route,一个公开,一个由 token 保护,而且 URL 形态很重要,因为 reveal URL 是你要保留的内容。gr.Server 适用于这里,因为它底层是一个 FastAPI app —— 这也是为什么 @server.api 和普通 @server.get 可以在同一进程中并排存在。注意:这也可以用 gr.Blocks() 通过使用 FastAPI 挂载自定义 route来构建:
# 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))
一个 daemon thread 每 30 秒清除过期 paste。整个服务(包括存储)大约只有 200 行应用代码,因为所有内容都在同一个进程中。
gradio.Server 提供了什么
三个 app 的划分方式都相同——任何触及模型的内容都通过 @server.api,其余部分保留在普通 FastAPI route 上:
| App | Queued compute (@server.api) |
普通 FastAPI route |
|---|---|---|
| Document Privacy Explorer | analyze_document — 提取、检测、stats |
GET / 提供自定义阅读视图 |
| Image Anonymizer | anonymize_screenshot — OCR、检测、span → 像素框 |
GET / + GET /examples/* 提供 canvas UI 和预加载示例 |
| SmartRedact Paste | create_paste — 检测、脱敏、生成 ID |
GET / compose page,GET /view/{pid}?token=... 公开视图 + token 保护视图,GET /api/paste/{pid} JSON lookup |
@server.api 提供 Gradio 的 queue(序列化请求、在 ZeroGPU 上正确组合 @spaces.GPU、progress event),浏览器也是通过 @gradio/client 命中它。同一个 endpoint 也可供 gradio_client 用户从 Python 调用——一个函数,两个 SDK,没有重复代码。普通 @server.get/@server.post 则保留给静态表面:HTML 页面、文件 lookup、廉价的 dict 读取。这是 gradio.Server 介绍文章中的经验法则,也正是它让这三个 UI 非常不同的 app 仍然保持一致的原因。
试用
放入一份简历、一张 Slack 线程截图、一行包含 token 的日志。真正有意思的是看看 Privacy Filter 在你实际关心的文本中能捕捉到什么(以及偶尔漏掉什么)。
推荐阅读
- OpenAI 的发布文章:Introducing OpenAI Privacy Filter
- Model card:Hugging Face 上的 openai/privacy-filter
- Model card 中的脱敏示例和分类体系