Audio redaction detects and removes PII spoken in audio recordings (.mp3, .wav). Unlike the document formats, it relies on AWS speech services end to end: Amazon Transcribe converts speech to timestamped text, a Bedrock model detects PII in that text, Amazon Polly synthesizes replacement speech (or silence is inserted), and ffmpeg splices the replacements back into the audio at the exact PII timestamps.
The audio path reuses the same 3-step Step Functions pipeline as every other format (Detect → Synthetic → Redact) and the same synthetic-generation engine: only the detection and redaction processors are audio-specific.
S3 audio (.mp3/.wav)
│
▼ Step 1: Detection (detect_pii_audio)
├─ Amazon Transcribe → word-level timestamps + speaker diarization
├─ Bedrock LLM → detect PII in the transcript text
▼
Detection JSON {detections, transcript, words, speaker_segments}
│
▼ Step 2: Synthetic (batch_generate_synthetic_pii, shared engine)
PII → realistic fake replacements
│
▼ Step 3: Redaction (redact_audio)
├─ map PII text → timed word spans
├─ synthetic mode: Amazon Polly speech | silence mode: muted segment
├─ ffmpeg splices replacements at exact timestamps
▼
S3 output: anonymized WAV + speaker-formatted redacted transcript
DynamoDB status (identical to all formats): IN_PROGRESS → DETECTING → DETECT_COMPLETE → GENERATING_SYNTHETIC → SYNTHETIC_COMPLETE → REDACTING → COMPLETE.
Audio uses the same extension dispatch as the rest of the pipeline:
- Router (
router_handler.py) accepts.mp3and.wavas processable uploads. - Detection (
pii_detection_handler.py) maps.mp3/.wav→detect_pii_audio. - Redaction (
redact_handler.py) routes files whosefile_type == "audio"→redact_audio.
All audio logic lives in src/processors/audio_processor.py.
detect_pii_audio(source_bucket, source_key, config, bedrock_runtime, s3_client):
-
Transcribe (
_transcribe_from_s3): starts an Amazon Transcribe job that reads the file directly from S3 (the bucket the router uploaded to), with:MediaFileUri = s3://{source_bucket}/{source_key}MediaFormatderived from the file extension- Speaker diarization enabled (
ShowSpeakerLabels) for conversational readability LanguageCodefromaudio.language_code(defaulten-US)
Region note: Transcribe must run in the same region as the input bucket. The job uses the region from
AWS_DEFAULT_REGION/AWS_REGION. If they differ from the bucket's region you getBadRequestException: incorrect S3 URI. -
Word extraction (
_extract_words): pulls word-level items withstart/endtimestamps from the Transcribe result. Speaker segments come fromresults.speaker_labels.segments. -
PII detection: a single Bedrock call over the full transcript text via the shared model router (
converse_or_responses), so it honours every per-model rule (Claude / Nova / OpenAI GPT-5.x, thinking / reasoning effort). It uses the standard detection inference config (get_inference_config_from_yaml), not a hardcodedmaxTokens, so long transcripts don't truncate andmaxTokensstays above any extended-thinking budget. The response is read withsafe_get_response_content, which skips reasoning blocks and warns on truncation (the same extractor used by the text and vision paths).
Returns the standard detection payload:
{
"detections": [{"content", "type", "confidence", "detection_source"}, ...],
"token_usage": {...},
"file_type": "audio",
"transcript": "...", # full transcript text
"words": [...], # word-level items with timestamps
"speaker_segments": [...], # diarization segments
}By default the detection model is model.id from config.yaml; set audio.detection_model to override it for audio only.
Audio uses the same synthetic generator as every other format: batch_generate_synthetic_pii on the detected entities. See Synthetic Generation for clustering, batching, the repair pipeline, and the Faker fallback. No audio-specific logic here; the detected PII strings are replaced with realistic fakes (e.g. a phone number → a different valid-looking phone number) so the redacted audio still sounds natural.
redact_audio(s3_client, source_bucket, source_key, output_bucket, pii_mapping, detections, detection_data, config, job_id, output_prefix):
-
Locate PII spans (
_find_pii_spans): matches each detected PII string to the timed words from Transcribe, producing{start, end, fake_text}spans. Each detection is also annotated withtimestamp_start/timestamp_endfor the redaction report. -
Build replacement segments per
audio.redaction_mode:synthetic(default): Amazon Polly (_synthesize_replacement) synthesizes speech for the fake text usingaudio.polly_voice(defaultJoanna).silence: a silent segment (_generate_silence) the length of the PII span.
-
Splice (
_splice_audio): ffmpeg cuts the original audio at the PII timestamps and concatenates the surrounding audio with the replacement/silence segments, producing a clean anonymized track. If no spans are found, the original is simply transcoded to a normalized mono WAV. -
Speaker-formatted transcript (
_build_speaker_transcript): groups words by diarization segment into a readablespk_0: ... / spk_1: ...conversation, with PII replaced by the synthetic values.
Returns (output_key, replaced_count, found_originals).
Audio splicing shells out to the ffmpeg binary (not a Python package). The path resolves as:
FFMPEG = shutil.which("ffmpeg") or "/opt/bin/ffmpeg"- In Lambda: provided by the ffmpeg Lambda layer, which mounts at
/opt/bin/ffmpeg. - Locally (notebook / testing): install via
brew install ffmpeg(macOS) or your package manager, and make sure its directory is onPATHsoshutil.whichfinds it (otherwise it falls back to the Lambda path and fails withNo such file or directory: '/opt/bin/ffmpeg').
For an input dialog.mp3, audio redaction writes (under the job's output prefix):
| File | Purpose |
|---|---|
redacted/audio/dialog_anonymized.wav |
The PII-removed audio (WAV, mono, audio.sample_rate) |
redacted/audio/dialog_redacted_transcript.txt |
Speaker-formatted transcript with PII replaced |
intermediate/audio/dialog_original_transcript.txt |
Original speaker-formatted transcript (review/audit) |
Output is always WAV regardless of input format.
The audio: section of src/config.yaml:
audio:
redaction_mode: "synthetic" # "synthetic" (Polly speech) or "silence" (mute PII)
polly_voice: "Joanna" # Amazon Polly voice for synthetic replacements
sample_rate: "16000" # output WAV sample rate (Hz)
language_code: "en-US" # Amazon Transcribe language
# detection_model: "..." # optional: override the PII detection model for audio onlySee Configuration for the full reference.
The audio path requires permissions for, in addition to S3 and Bedrock:
- Amazon Transcribe:
transcribe:StartTranscriptionJob,transcribe:GetTranscriptionJob - Amazon Polly:
polly:SynthesizeSpeech
These are granted to the detection and redaction roles in both the CloudFormation and Terraform deployments. See Security and IAM Permissions.
- Output is WAV, even for
.mp3input. - English-tuned by default: set
audio.language_codefor other Transcribe-supported languages (the detection prompt is still English-oriented; adjust prompts for non-English content). - Detection accuracy depends on transcription accuracy: heavy accents, overlapping speech, or poor audio quality reduce both transcription and PII-detection quality. Always validate redacted output (see the disclaimer in the main README).
- Span matching is text-based: PII is removed where the transcribed words match a detection; mis-transcribed PII may not be located. Use
silencemode when maximum certainty is required. - Region: Transcribe and the input bucket must be in the same region.