Skip to content

PII redaction

The problem. Call transcripts end up in a CRM, a data warehouse, an LLM prompt. If they contain card numbers, phone numbers or names, all of those systems now hold personal data, with the compliance work that comes with it. PII redaction removes that data from the final transcripts before you receive them.

Where it works. Every model.

{ "pii": true }

By default, the pii preset is redacted: names, contact details, identity, card and account numbers, dates of birth and the like.

Each program streams a 16-bit WAV file and prints what comes back. Put your API key in the GLADIA_API_KEY environment variable, then run python pii.py meeting.wav.

pii.py
# pip install gladiaio-sdk requests
import os
import sys
import threading
import wave
import requests
from gladiaio_sdk import GladiaClient, LiveV2ConnectSessionOptions
API_URL = os.environ.get("GLADIA_API_URL", "https://eu1.api.gladia.io")
API_KEY = os.environ["GLADIA_API_KEY"]
with wave.open(sys.argv[1]) as wav: # a 16-bit PCM WAV file
rate, channels = wav.getframerate(), wav.getnchannels()
audio = wav.readframes(wav.getnframes())
config = {
"encoding": "wav/pcm",
"sample_rate": rate,
"bit_depth": 16,
"channels": channels,
"model": "solaria-3",
"language_config": {
"languages": [
"en"
]
},
"numerals": True,
"pii": {
"entities": [
"pii",
"pci"
],
"mode": "marker"
}
}
client = GladiaClient(api_key=API_KEY, api_url=API_URL)
# `pii` is a Gladia Enterprise option the Python SDK does not send yet
# (gladiaio/sdk#61): create the session over HTTP, then let the SDK stream it.
created = requests.post(f"{API_URL}/v2/live", headers={"x-gladia-key": API_KEY}, json=config, timeout=10)
created.raise_for_status()
info = created.json()
session = client.live_v2().connect_session(LiveV2ConnectSessionOptions(id=info["id"], url=info["url"]))
ended, errors = threading.Event(), []
@session.on("message")
def on_message(message):
if message.type == "transcript" and message.data.is_final:
print(message.data.utterance.text)
def on_error(error):
if "Unsupported websocket message type" not in str(error): # pii messages: gladiaio/sdk#61
errors.append(error)
session.on("error", on_error)
session.once("ended", lambda _: ended.set())
step = rate * channels * 2 // 10 # 100 ms of audio per chunk
for start in range(0, len(audio), step):
session.send_audio(audio[start : start + step]) # live audio: send each chunk as it is captured
session.stop_recording()
ended.wait(timeout=60)
sys.exit(1 if errors or not ended.is_set() else 0)
{
"type": "transcript",
"data": {
"is_final": true,
"utterance": {
"text": "My card number is <pii:credit_card id=\"1\"/> and my email <pii:email_address id=\"1\"/>.",
"words": [{ "word": "", "start": 3.1, "end": 3.4, "confidence": 0.95 }]
}
}
}

Right after it, a pii message lists what was redacted (WebSocket only):

{
"type": "pii",
"error": null,
"data": {
"utterance_id": "00_00000005",
"results": [
{ "entity_type": "credit_card", "id": 1, "action": "marker", "char_start": 18, "char_end": 45 },
{ "entity_type": "email_address", "id": 1, "action": "marker", "char_start": 59, "char_end": 88 }
]
}
}
{ "pii": { "entities": ["pii", "pci"], "mode": "mask", "mask_character": "#" } }
option default what it does
entities pii What to redact: presets (pii, pci, phi, contact, financial, name, location…), types, or {"enable": [...], "disable": [...]}.
mode marker marker: an element in place of the data (<pii:phone_number id="1"/>); mask: mask characters inside the element; tag: the data kept inside the element (tagging only, nothing hidden).
threshold 0.5 Confidence under which a span is not redacted. Lower: more redacted, more false positives.
mask_character * The character of mask mode.
coreference heuristics How mentions of the same data get the same id: heuristics, exact (same text only) or off.
return_entities false Also return the redacted text in the pii message (text), for your own vault.
  • No partial transcripts. Partials are never redacted, so with redaction on none is sent, on the WebSocket or to callbacks, even with receive_partial_transcripts: true. Plan your live display on finals. (mode: tag hides nothing and keeps partials.)
  • Words lose their text. A redacted final’s words keep their times and speakers, their word is empty, and text_raw is not sent.
  • The audio is not redacted. If your account records its audio, the recording still contains what was said. Ask for recording to be turned off if that matters to you (Audio recordings).
  • custom_metadata is not redacted. Do not put personal data in it.
  • Not with translation. A session with both pii and translation is refused when it starts (WebSocket 1011, rejected: ...).
  • Finals may come a little later while a number or an email is still being dictated, so it is redacted whole.
  • Detection is statistical. A rare format, a name spelled letter by letter, or poor audio can slip through, and ordinary words can be redacted. Tune entities and threshold on your own calls before production.
  • return_entities brings the data back. Use it only to store the values somewhere safer than your transcripts.
  • The pii message is not stored with the session’s result and is not sent to callbacks.
symptom cause fix
no partial transcripts anymore redaction on expected; use finals, or mode: tag if you only need tagging
WebSocket closed 1011 “rejected: … pii redaction” translation also on drop one of them
a card number is not redacted spoken digit by digit with long pauses, or another preset keep numerals and the number hold on, add pci
ordinary words are redacted low confidence spans raise threshold, narrow entities