跳到正文
原文
Hugging Face Blog·· 2026-04-27精选AI 评分60

如何用 OpenAI Privacy Filter 构建可扩展 Web 应用

How to build scalable web apps with OpenAI's Privacy Filter

AI 摘要

OpenAI 本周在 Hugging Face Hub 发布开源 PII 检测器 Privacy Filter,可在单次 128k 上下文前向传播中标注八类个人信息,模型为 1.5B 参数、50M 激活参数,Apache 2.0 许可,在 PII-Masking-300k 基准上达到 SOTA。

正文 · AI 翻译

OpenAI 本周在 Hub 上发布了 Privacy Filter:一个开源的个人可识别信息(PII)检测器,可在一次 128k 上下文的前向传播中为文本标注八个类别。模型卡。我们花了几个小时用它构建应用,最终做出了三个应用,每个都展示了它不同层面的能力。

  • 文档隐私浏览器:拖入一个 PDF 或 DOCX,即可回读文档,所有 PII 片段都会在原位高亮显示。
  • 图像匿名化工具:上传一张图片,返回时姓名、邮箱和账号上会覆盖黑色遮挡条。图片还可以在画布上编辑,以便你在下载前添加自己的标注。
  • SmartRedact Paste:粘贴敏感文本,分享一个提供脱敏版本的公开 URL,同时为自己保留一个私密的查看链接。

这三个应用都构建在 gradio.Server 之上,它让你可以将自定义 HTML/JS 前端与 Gradio 的队列、ZeroGPU 分配以及 gradio_client SDK 配对使用。在所有这些应用中,gradio.Server 都扮演相同的后端角色,而正是这种一致性让它真正强大。

模型

Privacy Filter 是一个 1.5B 参数模型,拥有 50M 活跃参数,采用 Apache 2.0 宽松许可。PII 类别为 private_person、private_address、private_email、private_phone、private_url、private_date、account_number、secret。上下文长度为 128,000 个 token。在 PII-Masking-300k 基准测试上达到了最先进的性能。完整数据和方法论见官方发布博客。

1. 文档隐私浏览器

在 ysharma/OPF-Document-PII-Explorer 上试用。

用户问题。你想阅读一份 PII 密集的文档(合同、简历、导出的聊天记录),其中每个检测到的片段都按类别高亮显示,侧边栏有筛选器,顶部有摘要仪表盘。阅读体验应该像普通文档,而不是表单。

Privacy Filter 在这里的作用。整个文件在一次 128k 上下文的前向传播中处理完毕,因此无需分块、无需拼接,片段偏移量与渲染文本直接对齐。BIOES 解码可在长而模糊的连续片段中保持片段边界清晰。

gr.Server 在这里的作用。你可以在 Blocks 中用 gr.HighlightedText 和侧边栏来实现,也能用。但我们想要的阅读体验(衬线正文、在客户端切换 CSS 类而不重新运行模型的类别筛选器、不会强制页面重新渲染的摘要仪表盘)手写比组合更容易实现。gr.Server 让我们可以将阅读器视图作为单个 HTML 文件提供,并将模型暴露在一个排队端点之后:

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),
    }

注意这个装饰器:是 @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 上试用。

用户问题。你想分享一张图片或任何截图(Slack 对话、收据、Stripe 仪表盘),并在 PII 上覆盖黑色遮挡条。你希望可以开关遮挡条、拖动它们重新定位,或者为模型遗漏的任何内容手动画一个,然后导出结果。

Privacy Filter 在这里的作用。 Tesseract 运行 OCR 并返回每个词的边界框。后端通过字符偏移到框的映射重建完整文本,然后对整个文本运行一次 Privacy Filter。检测到的字符跨度会对照词映射进行查找,并按行合并为像素矩形。

gr.Server 在这里的作用。 gr.ImageEditor 支持分层标注,是图像打码的合理起点。我们想要的工作流(每个条形的类别元数据、一键切换某个类别中的所有条形、在客户端以原始分辨率导出 PNG 而无需服务器往返)在自定义 <canvas> 前端上构建起来更干净。gr.Server 从一个排队端点返回像素矩形,并让 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}, ...]
    }

前端使用 client.predict("/anonymize_screenshot", { image: handle_file(file) }) 调用它,与上面的模式相同。切换、拖动、绘制新条形以及 PNG 导出都在浏览器中进行;编辑永远不会往返到服务器。

3. SmartRedact Paste

在 ysharma/OPF-SmartRedact-Paste 试用。

用户问题。 你想要一个在分享前进行打码的 pastebin。你粘贴一行日志、一封电子邮件、一张支持工单。你会得到两个 URL。公开的那个提供打码后的版本,带有 <PRIVATE_PERSON>、<PRIVATE_EMAIL>、<ACCOUNT_NUMBER> 占位符,遵循 官方博客示例 中的打码约定。私有的那个由你保留的令牌保护,显示带有高亮跨度的原始内容。

Privacy Filter 在这里的作用。 将每个检测到的跨度替换为存储粘贴内容上的 <CATEGORY> 占位符。这就是整个打码步骤。多语言文本(模型卡示例中的西班牙语、法语、中文、印地语等)通过同一调用处理,无需更改。

gr.Server 在这里的作用。 这个应用需要针对同一个粘贴 ID 的两条不同的 GET 路由,一条公开,一条令牌保护,而且 URL 形状很重要,因为揭示 URL 是你保留的东西。gr.Server 在这里可行,因为它底层是一个 FastAPI 应用——这也是为什么 @server.api 和普通的 @server.get 可以在同一进程中并排存在。注意:这也可以通过 使用 FastAPI 挂载自定义路由 用 gr.Blocks() 构建:

# 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))

一个守护线程每 30 秒清除过期的粘贴。整个服务,包括存储,大约 200 行应用代码,因为一切都存在于一个进程中。

gradio.Server 提供什么

所有三个应用的分工是相同的——任何触及模型的内容都经过 @server.api,其他一切留在普通 FastAPI 路由上:

应用 排队计算(@server.api) 普通 FastAPI 路由
Document Privacy Explorer analyze_document — 提取、检测、统计 GET / 提供自定义阅读器视图
Image Anonymizer anonymize_screenshot — OCR、检测、跨度 → 像素框 GET / + GET /examples/* 提供 canvas UI 和预加载示例
SmartRedact Paste create_paste — 检测、打码、生成 ID GET / 组合页面,GET /view/{pid}?token=... 公开 + 令牌保护视图,GET /api/paste/{pid} JSON 查找

@server.api 为你提供 Gradio 的队列(序列化请求、在 ZeroGPU 上正确的 @spaces.GPU 组合、进度事件),并且它是浏览器通过 @gradio/client 访问的内容。同一个端点也是 gradio_client 用户从 Python 访问的内容——一个函数,两个 SDK,没有重复代码。普通的 @server.get/@server.post 保留给静态表面:HTML 页面、文件查找、廉价的字典读取。这是来自 gradio.Server 介绍文章 的经验法则,也正是它让这三个应用尽管 UI 非常不同却感觉一致。

试用它们

把简历、Slack 对话截图、含有 token 的日志行拖进来。有趣的地方在于看看 Privacy Filter 在你真正关心的文本上能捕捉到什么(偶尔也会漏掉什么)。

推荐阅读

来源:Hugging Face Blog · huggingface.co

00