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.
Step by step
Section titled “Step by step”1. Turn it on
Section titled “1. Turn it on”{ "pii": true }By default, the pii preset is redacted: names, contact details, identity, card and account numbers, dates of
birth and the like.
2. Start a session with it
Section titled “2. Start a session with it”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.
# pip install gladiaio-sdk requestsimport osimport sysimport threadingimport wave
import requestsfrom 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 chunkfor start in range(0, len(audio), step): session.send_audio(audio[start : start + step]) # live audio: send each chunk as it is capturedsession.stop_recording()
ended.wait(timeout=60)sys.exit(1 if errors or not ended.is_set() else 0)// npm install @gladiaio/sdkimport { readFileSync } from "node:fs";import { GladiaClient } from "@gladiaio/sdk";
const { sampleRate, channels, pcm } = readWav(process.argv[2]); // a 16-bit PCM WAV file
const config = { encoding: "wav/pcm", sample_rate: sampleRate, bit_depth: 16, channels, model: "solaria-3", language_config: { languages: [ "en" ] }, numerals: true, pii: { entities: [ "pii", "pci" ], mode: "marker" }};
const client = new GladiaClient({ apiKey: process.env.GLADIA_API_KEY, apiUrl: process.env.GLADIA_API_URL ?? "https://eu1.api.gladia.io",});// `pii` is a Gladia Enterprise option not in the SDK's types yet: the SDK sends it as is.const session = client.liveV2().startSession(config);
session.on("message", (message) => { if (message.type === "transcript" && message.data.is_final) console.log(message.data.utterance.text); if (message.type === "pii") { for (const item of message.data.results) console.log(` redacted ${item.entity_type} #${item.id}`); }});session.on("error", (error) => { console.error(error); process.exitCode = 1;});session.once("ended", () => process.exit());
const step = (sampleRate * channels * 2) / 10; // 100 ms of audio per chunkfor (let start = 0; start < pcm.length; start += step) { session.sendAudio(pcm.subarray(start, start + step)); // live audio: send each chunk as it is captured}session.stopRecording();
function readWav(path) { const file = readFileSync(path); let fmt, pcm; for (let pos = 12; pos < file.length; ) { const id = file.toString("ascii", pos, pos + 4); const size = file.readUInt32LE(pos + 4); if (id === "fmt ") fmt = { channels: file.readUInt16LE(pos + 10), sampleRate: file.readUInt32LE(pos + 12) }; if (id === "data") pcm = file.subarray(pos + 8, pos + 8 + size); pos += 8 + size + (size % 2); } return { ...fmt, pcm };}Create the session, then stream its audio to the returned url (Live WebSocket):
curl -X POST https://eu1.api.gladia.io/v2/live \ -H "x-gladia-key: $GLADIA_API_KEY" \ -H "content-type: application/json" \ -d '{ "encoding": "wav/pcm", "sample_rate": 16000, "bit_depth": 16, "channels": 1, "model": "solaria-3", "language_config": { "languages": [ "en" ] }, "numerals": true, "pii": { "entities": [ "pii", "pci" ], "mode": "marker" } }'3. Read the redacted transcript
Section titled “3. Read the redacted transcript”{ "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 } ] }}Options
Section titled “Options”{ "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. |
Limits and gotchas
Section titled “Limits and gotchas”- 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: taghides nothing and keeps partials.) - Words lose their text. A redacted final’s words keep their times and speakers, their
wordis empty, andtext_rawis 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_metadatais not redacted. Do not put personal data in it.- Not with translation. A session with both
piiandtranslationis refused when it starts (WebSocket1011,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
entitiesandthresholdon your own calls before production. return_entitiesbrings the data back. Use it only to store the values somewhere safer than your transcripts.- The
piimessage is not stored with the session’s result and is not sent to callbacks.
Troubleshooting
Section titled “Troubleshooting”| 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 |