一声棒喝,本不立文字
偏要著録,已是二义

huggingface-blog

如何使用 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)

图片 2:Freddy Boulton 的头像

图片 3:Abubakar Abid 的头像

OpenAI 本周在 Hub 上发布了 Privacy Filter:一个开源的 personally-identifiable information(PII,个人身份信息)检测器,可在一次 128k context 的 forward pass 中对文本进行八类标注。Model card。我们花了几个小时基于它构建应用,最后做出了三个 app,每个 app 展示了它能力的不同侧面。

这三个 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_personprivate_addressprivate_emailprivate_phoneprivate_urlprivate_dateaccount_numbersecret。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 在你实际关心的文本中能捕捉到什么(以及偶尔漏掉什么)。

推荐阅读

译自 huggingface-blog · 录于 二〇二六年五月三日