openai-privacy-filter와 Gradio의 gradio.Server를 조합하면 문서·이미지·텍스트의 개인식별정보(PII)를 처리하는 세 가지 앱을 빠르게 구현할 수 있다. Privacy Filter는 1.5B 파라미터(활성 50M)로 128K 컨텍스트를 단일 순전파에서 처리하며, Apache 2.0으로 무료 사용 가능하다.
gradio.Server 구조 이해
세 앱 모두 동일한 패턴을 사용한다.
| 용도 | 데코레이터 | 특징 |
|---|---|---|
| 모델 연산 | @server.api(name="...") | Gradio 큐, ZeroGPU 지원, Python·JS 양쪽에서 호출 가능 |
| 정적 페이지·라우팅 | @server.get(...) | 일반 FastAPI 라우트, 큐 불필요 |
import gradio as gr
server = gr.Server()
@server.api(name="analyze")
def analyze(text: str) -> dict:
# Privacy Filter 호출
return run_privacy_filter(text)
@server.get("/", response_class=HTMLResponse)
async def homepage():
return FRONTEND_HTML브라우저에서는 @gradio/client JS SDK로, Python 클라이언트에서는 gradio_client로 동일 엔드포인트에 접근한다.
앱 1: Document Privacy Explorer
목적: PDF 또는 DOCX를 업로드하면 탐지된 PII 스팬을 카테고리별로 하이라이트해서 보여준다.
핵심 아이디어: 128K 컨텍스트 덕분에 파일 전체를 청킹 없이 단일 패스로 처리한다. 스팬 오프셋이 렌더링된 텍스트와 직접 정렬된다.
@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)
return {
"text": source_text,
"spans": spans, # [{start, end, label}, ...]
"stats": compute_stats(source_text, spans),
}<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>프론트엔드는 순수 HTML/JS로 작성해 카테고리 필터 토글, CSS 클래스 전환 등 인터랙션을 서버 재요청 없이 처리한다.
앱 2: Image Anonymizer
목적: 이미지(스크린샷, 영수증, 슬랙 화면 등)를 업로드하면 PII가 검출된 영역에 블랙바를 씌워 돌려준다. 캔버스에서 수동 조정 후 다운로드 가능.
핵심 아이디어: Tesseract OCR로 단어별 바운딩 박스를 추출 → Privacy Filter로 텍스트 스팬 탐지 → 캐릭터 오프셋을 픽셀 좌표로 변환
@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}, ...]
}토글·드래그·새 박스 그리기·PNG 내보내기는 모두 브라우저 캔버스에서 처리되며 서버에 재요청하지 않는다.
앱 3: SmartRedact Paste
목적: 민감한 텍스트를 붙여넣으면 리댁션된 공개 URL과 원본을 볼 수 있는 비공개 URL 두 개를 발급한다.
핵심 아이디어: Privacy Filter가 스팬을 <CATEGORY> 플레이스홀더로 교체 → FastAPI 라우트로 공개/비공개 뷰를 분리
@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) # <PRIVATE_PERSON>, <PRIVATE_EMAIL> 등
pid = secrets.token_urlsafe(6)
reveal_token = secrets.token_urlsafe(22)
PASTES[pid] = Paste(pid, reveal_token, source_text, redacted, spans, ...)
return {
"view_path": f"/view/{pid}",
"reveal_path": f"/view/{pid}?token={reveal_token}",
}
@server.get("/view/{pid}", response_class=HTMLResponse)
async def view_paste(pid: str, token: str | None = None):
p = PASTES.get(pid)
revealed = bool(token) and secrets.compare_digest(token, p.reveal_token)
return HTMLResponse(render_view(p, revealed))전체 서비스(스토리지 포함)가 약 200줄의 애플리케이션 코드로 구현된다. 만료된 페이스트는 30초 주기 데몬 스레드가 자동 삭제한다.
탐지 카테고리
Privacy Filter는 8가지 범주를 탐지한다.
private_person · private_address · private_email · private_phone · private_url · private_date · account_number · secret
다국어 텍스트(영어·스페인어·프랑스어·중국어·힌디어 등)도 동일한 호출로 처리한다.
직접 체험
참고 자료
- How to build scalable web apps with OpenAI’s Privacy Filter — Hugging Face Blog (2026-04-27)
- openai/privacy-filter — Hugging Face 모델 카드
- Introducing OpenAI Privacy Filter — OpenAI 공식 발표