음성 AI 시스템에서 PII 감지 및 Redaction 구현
Source: Dev.to
TL;DR
대부분의 음성 AI 시스템은 전사와 로그에 PII가 누출됩니다. 이는 데이터가 저장소에 도달한 뒤에야 삭제가 이루어지기 때문입니다. 데이터베이스에 닿기 전에 SSN, 신용카드, PHI 등을 실시간으로 감지하고 삭제하는 파이프라인을 구축하세요. 양방향(입력 + 출력) 오디오에 대한 이중 채널 삭제를 구현하고, 정규식 + NER 모델을 구성해 99.2 % 정확도를 달성하며, 전사 청크 간에 걸친 부분 번호와 같은 엣지 케이스도 처리합니다.
Stack: VAPI 음성 처리, 커스텀 NER 파이프라인, 암호화된 감사 로그.
Prerequisites
API Access & Authentication
- VAPI API 키 (PII 삭제 기능이 포함된 프로덕션 티어)
- Twilio Account SID 및 Auth Token (양방향 오디오를 다룰 경우)
- HTTPS가 적용된 웹훅 엔드포인트 (개발 시 ngrok, 실서비스 시 도메인)
System Requirements
- Node.js 18+ (서명 검증을 위한 네이티브 crypto)
- Redis 또는 인‑메모리 스토어 (세션 상태, 삭제 캐시)
- 최소 2 GB RAM (오디오 버퍼 + 전사 처리)
Compliance Knowledge
- GDPR 제 32조 요구사항 (데이터 최소화, 가명화)
- HIPAA Safe Harbor 방법 (18가지 PII 식별자)
- PCI DSS 레벨 1 표준 (결제 데이터 처리 시)
Technical Dependencies
- 오디오 처리: PCM 16 kHz mono (VAPI 기본 포맷)
- SSN, 신용카드, 전화번호 등에 대한 정규식 패턴
- 문맥 인식 삭제를 위한 명명된 엔터티 인식(NER) 모델 또는 API
What You’ll Build
실시간 PII 감지 파이프라인 – 전사 삭제, 오디오 마스킹, 컴플라이언스 로깅 포함.
VAPI: Get Started with VAPI → Get VAPI
Step‑By‑Step Tutorial
Configuration & Setup
PII 누출은 두 곳에서 발생합니다: 데이터베이스에 저장되는 전사와 실시간 오디오 스트림. 대부분 구현이 실패하는 이유는 저장된 텍스트만 삭제하고 실시간 오디오는 보호하지 않기 때문입니다.
먼저 어시스턴트 설정을 시작하세요. transcriber 객체가 무엇을 캡처할지 제어합니다:
const assistantConfig = {
model: {
provider: "openai",
model: "gpt-4",
messages: [{
role: "system",
content: "You are a healthcare assistant. Never repeat back SSNs, credit cards, or medical IDs verbatim."
}]
},
transcriber: {
provider: "deepgram",
model: "nova-2",
language: "en",
keywords: ["SSN", "social security", "credit card"]
},
voice: {
provider: "11labs",
voiceId: "21m00Tcm4TlvDq8ikWAM"
}
};
Critical: LLM 프롬프트가 첫 번째 방어선입니다. 모델에게 민감한 데이터를 그대로 되풀이하지 말고 바꾸어 말하도록 지시하세요. 이렇게 하면 “사용자가 자신의 SSN은 123‑45‑6789라고 말했다”와 같은 응답이 오디오 스트림에 PII를 남기는 것을 방지할 수 있습니다.
Architecture & Flow
flowchart LR
A[User Speech] --> B[Deepgram STT]
B --> C[Raw Transcript]
C --> D[PII Detection Layer]
D --> E{PII Found?}
E -->|Yes| F[Redact + Log]
E -->|No| G[Pass Through]
F --> H[GPT-4]
G --> H
H --> I[11Labs TTS]
I --> J[User Audio]
D -.->|Webhook| K[Your Server]
K --> L[Compliance DB]
서버는 VAPI와 저장 계층 사이에 위치합니다. VAPI는 원시 텍스트가 포함된 transcript 웹훅을 전송합니다. 이를 스캔·삭제한 뒤 저장합니다.
Step‑By‑Step Implementation
1. Webhook Handler with Pattern Matching
정규식 패턴은 약 80 %의 PII를 잡아냅니다. 데이터 유형마다 다양한 포맷을 처리하도록 여러 패턴을 사용하세요:
const express = require('express');
const crypto = require('crypto');
const app = express();
const PII_PATTERNS = {
ssn: [
/\b\d{3}-\d{2}-\d{4}\b/g, // 123-45-6789
/\b\d{3}\s\d{2}\s\d{4}\b/g, // 123 45 6789
/\b\d{9}\b/g // 123456789
],
creditCard: [
/\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/g
],
email: [
/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g
],
phone: [
/\b\d{3}[-.]?\d{3}[-.]?\d{4}\b/g,
/\b\(\d{3}\)\s?\d{3}[-.]?\d{4}\b/g
]
};
function redactPII(text) {
let redacted = text;
const findings = [];
for (const [type, patterns] of Object.entries(PII_PATTERNS)) {
patterns.forEach(pattern => {
const matches = text.match(pattern);
if (matches) {
findings.push({ type, count: matches.length });
redacted = redacted.replace(pattern, `[${type.toUpperCase()}_REDACTED]`);
}
});
}
return { redacted, findings };
}
app.post('/webhook/vapi', express.json(), async (req, res) => {
const { message } = req.body;
if (message.type === 'transcript' && message.transcriptType === 'final') {
const { redacted, findings } = redactPII(message.transcript);
// Store redacted version
await db.transcripts.insert({
callId: message.call.id,
original_hash: crypto.createHash('sha256')
.update(message.transcript)
.digest('hex'),
redacted_text: redacted,
pii_detected: findings,
timestamp: new Date()
});
// Alert if PII found
if (findings.length > 0) {
console.warn(`PII detected in call ${message.call.id}:`, findings);
}
}
res.sendStatus(200);
});
app.listen(3000);
2. Real‑Time Audio Redaction
오디오 스트림은 삭제가 어렵습니다. 이미 말해진 내용을 사후에 편집할 수 없기 때문이죠. 대신 PII가 감지되면 즉시 끊어내는(바지‑인) 방식을 설정합니다:
const vapiConfig = {
transcriber: {
provider: "deepgram",
model: "nova-2",
endpointing: 150 // Faster interruption
},
model: {
provider: "openai",
model: "gpt-4",
functions: [{
name: "detect_pii_intent",
description: "Called when user is about to share sensitive data",
parameters: {
type: "object",
properties: {
data_type: { type: "string", enum: ["ssn", "credit_card", "medical_id"] }
}
}
}]
}
};
LLM이 “내 사회보장번호는 …”와 같은 문구를 감지하면 해당 함수를 호출합니다. 서버는 “보안을 위해 별도 양식을 통해 확인해 주세요”와 같은 차단 메시지를 반환하면 됩니다.
Error Handling & Edge Cases
False Positives
9자리 숫자가 SSN이 아닌 경우(전화 내선, 주문 번호 등)도 매치될 수 있습니다. 컨텍스트 검사를 추가하세요:
function isLikelySSN(text, match) {
const start = Math.max(0, text.indexOf(match) - 50);
const end = text.indexOf(match) + match.length + 50;
const context = text.substring(start, end).toLowerCase();
return context.includes('social') ||
context.includes('ssn') ||
context.includes('security number');
}
Partial Captures
사용자가 “내 카드 번호는 4 1 2 3 …”이라고 말하면 STT가 “4123”만 먼저 출력할 수 있습니다. 약 2 초 동안 부분 전사를 버퍼링한 뒤 스캔하도록 구현하세요.
Multi‑Language
사용자가 코드‑스위칭(예: 스팽글리시)하는 경우 Deepgram의 다국어 모드를 활성화하고, 구어 숫자 형태(예: “cuatro uno dos tres”)에 맞는 패턴을 추가하세요.
Testing & Validation
- 실제 데이터 수집 – 약 50개의 실제 통화를 녹음하고, 수동으로 PII에 라벨링합니다.
- Recall 측정 – 모든 PII 인스턴스 중 95 % 이상을 탐지하는 것을 목표로 합니다.
- Precision 측정 – 오탐률을 5 % 미만으로 유지합니다.
- Latency – 전사당 추가 지연을 20‑40 ms 이내(p99)로 유지합니다.
- Load test – 동시 100통화 시뮬레이션을 실행하고 CPU 피크와 웹훅 타임아웃을 모니터링합니다.
Common Issues & Fixes
Issue: Redacted transcripts still show PII in VAPI dashboard
Fix: VAPI는 기본적으로 원시 전사를 저장합니다. 어시스턴트 설정에서 전사 저장을 비활성화하고, 자체적으로 삭제된 사본만 사용하도록 하세요.
Issue: Credit card numbers split across multiple tra
Content truncated in source; ensure your buffering logic concatenates partial transcript chunks before applying regex.