Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Implement audio data handling best practices for Deepgram integrations. Use when managing audio file storage, implementing data retention, or ensuring GDPR/HIPAA compliance for transcription data. Trigger: "deepgram data", "audio storage", "transcription data", "deepgram GDPR", "deepgram HIPAA", "deepgram privacy", "PII redaction".
.claude/skills/jeremylongshore-deepgram-data-handling/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 81% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 131% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 81% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 62% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 126% | 0% |
Best practices for handling audio and transcript data with Deepgram. Covers Deepgram's built-in redact parameter for PII, secure audio upload with encryption, transcript storage patterns, data retention policies, and GDPR/HIPAA compliance workflows.
| Deepgram Feature | What It Does | Enable | |-------------------|-------------|--------| | redact: ['pci'] | Masks credit card numbers in transcript | Query param | | redact: ['ssn'] | Masks Social Security numbers | Query param | | redact: ['numbers'] | Masks all numeric sequences | Query param | | Data retention | Deepgram does NOT store audio or transcripts | Default behavior |
Deepgram's data policy: Audio is processed in real-time and not stored. Transcripts are not retained unless you use Deepgram's optional storage features.
typescriptimport { createClient } from '@deepgram/sdk'; const deepgram = createClient(process.env.DEEPGRAM_API_KEY!); // Deepgram redacts PII directly during transcription const { result } = await deepgram.listen.prerecorded.transcribeUrl( { url: audioUrl }, { model: 'nova-3', smart_format: true, redact: ['pci', 'ssn'], // Credit cards + SSNs -> [REDACTED] } ); // Output: "My card is [REDACTED] and SSN is [REDACTED]" console.log(result.results.channels[0].alternatives[0].transcript); // For maximum privacy, redact all numbers: // redact: ['pci', 'ssn', 'numbers']
typescript// Additional redaction patterns beyond Deepgram's built-in const piiPatterns: Array<{ name: string; pattern: RegExp; replacement: string }> = [ { name: 'email', pattern: /\b[\w.-]+@[\w.-]+\.\w{2,}\b/g, replacement: '[EMAIL]' }, { name: 'phone', pattern: /\b(\+?\d{1,3}[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b/g, replacement: '[PHONE]' }, { name: 'dob', pattern: /\b(0[1-9]|1[0-2])\/.-\/.-\d{2}\b/g, replacement: '[DOB]' }, { name: 'address', pattern: /\b\d{1,5}\s[\w\s]+(?:Street|St|Avenue|Ave|Road|Rd|Drive|Dr|Lane|Ln|Boulevard|Blvd)\b/gi, replacement: '[ADDRESS]' }, ]; function redactPII(text: string): { redacted: string; found: string[] } { let redacted = text; const found: string[] = []; for (const { name, pattern, replacement } of piiPatterns) { const matches = text.match(pattern); if (matches) { found.push(`${name}: ${matches.length} occurrence(s)`); redacted = redacted.replace(pattern, replacement); } } return { redacted, found }; } // Usage after Deepgram transcription: const transcript = result.results.channels[0].alternatives[0].transcript; const { redacted, found } = redactPII(transcript); if (found.length > 0) console.log('PII found and redacted:', found);
typescriptimport { S3Client, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3'; import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; import { createHash, randomUUID } from 'crypto'; import { readFileSync } from 'fs'; const s3 = new S3Client({ region: process.env.AWS_REGION ?? 'us-east-1' }); const BUCKET = process.env.AUDIO_BUCKET!; async function uploadAudio(filePath: string, metadata: Record<string, string> = {}) { const audio = readFileSync(filePath); const checksum = createHash('sha256').update(audio).digest('hex'); const key = `audio/${randomUUID()}-${checksum.substring(0, 8)}.wav`; await s3.send(new PutObjectCommand({ Bucket: BUCKET, Key: key, Body: audio, ContentType: 'audio/wav', ServerSideEncryption: 'aws:kms', // Encrypt at rest Metadata: { ...metadata, checksum, uploadedAt: new Date().toISOString(), }, })); // Generate presigned URL for Deepgram to fetch (expires in 1 hour) const presignedUrl = await getSignedUrl(s3, new GetObjectCommand({ Bucket: BUCKET, Key: key }), { expiresIn: 3600 } ); return { key, checksum, presignedUrl }; } // Upload -> Get presigned URL -> Send to Deepgram const { presignedUrl } = await uploadAudio('./recording.wav', { source: 'call-center' }); const { result } = await deepgram.listen.prerecorded.transcribeUrl( { url: presignedUrl }, { model: 'nova-3', smart_format: true, redact: ['pci', 'ssn'] } );
typescriptinterface StoredTranscript { id: string; audioKey: string; // S3 reference requestId: string; // Deepgram request_id transcript: string; // Redacted text confidence: number; duration: number; // Audio duration in seconds model: string; speakers: number; utterances?: Array<{ speaker: number; text: string; start: number; end: number; }>; metadata: { redacted: boolean; piiTypesFound: string[]; createdAt: string; retentionPolicy: 'standard' | 'legal_hold' | 'hipaa'; expiresAt: string; }; } function buildTranscriptRecord( audioKey: string, result: any, retentionDays = 90 ): StoredTranscript { const alt = result.results.channels[0].alternatives[0]; const { redacted, found } = redactPII(alt.transcript); return { id: randomUUID(), audioKey, requestId: result.metadata.request_id, transcript: redacted, confidence: alt.confidence, duration: result.metadata.duration, model: Object.keys(result.metadata.model_info ?? {})[0] ?? 'unknown', speakers: new Set(alt.words?.map((w: any) => w.speaker).filter(Boolean)).size, utterances: result.results.utterances?.map((u: any) => ({ speaker: u.speaker, text: u.transcript, start: u.start, end: u.end, })), metadata: { redacted: found.length > 0, piiTypesFound: found, createdAt: new Date().toISOString(), retentionPolicy: 'standard', expiresAt: new Date(Date.now() + retentionDays * 86400000).toISOString(), }, }; }
typescriptconst retentionPolicies = { standard: { days: 90, description: 'Default retention' }, legal_hold: { days: 2555, description: '7 years for legal' }, hipaa: { days: 2190, description: '6 years per HIPAA' }, temp: { days: 7, description: 'Temporary processing' }, }; async function enforceRetention(db: any, s3Client: S3Client, bucket: string) { const now = new Date(); // Find expired transcripts const expired = await db.query( 'SELECT id, audio_key FROM transcripts WHERE expires_at < $1 AND retention_policy != $2', [now.toISOString(), 'legal_hold'] ); console.log(`Found ${expired.rows.length} expired transcripts`); for (const row of expired.rows) { // Delete audio from S3 try { await s3Client.send(new DeleteObjectCommand({ Bucket: bucket, Key: row.audio_key, })); } catch (err: any) { console.error(`S3 delete failed for ${row.audio_key}:`, err.message); } // Delete transcript from database await db.query('DELETE FROM transcripts WHERE id = $1', [row.id]); console.log(`Deleted: ${row.id}`); } return expired.rows.length; }
typescriptasync function processErasureRequest(userId: string, db: any, s3Client: S3Client, bucket: string) { console.log(`Processing GDPR erasure request for user: ${userId}`); // 1. Find all user transcripts const transcripts = await db.query( 'SELECT id, audio_key FROM transcripts WHERE user_id = $1', [userId] ); // 2. Delete audio files from S3 for (const row of transcripts.rows) { if (row.audio_key) { await s3Client.send(new DeleteObjectCommand({ Bucket: bucket, Key: row.audio_key })); } } // 3. Delete transcripts from database const deleted = await db.query('DELETE FROM transcripts WHERE user_id = $1', [userId]); // 4. Delete user metadata await db.query('DELETE FROM user_metadata WHERE user_id = $1', [userId]); // 5. Audit log (keep for compliance — does not contain PII) console.log(JSON.stringify({ action: 'gdpr_erasure', userId: userId.substring(0, 8) + '...', // Partial for audit transcriptsDeleted: deleted.rowCount, audioFilesDeleted: transcripts.rows.length, timestamp: new Date().toISOString(), })); return { transcriptsDeleted: deleted.rowCount, audioFilesDeleted: transcripts.rows.length }; }
| Issue | Cause | Solution | |-------|-------|----------| | PII still visible | Deepgram redact not set | Add redact: ['pci', 'ssn'] to options | | S3 upload fails | Missing KMS permissions | Add kms:GenerateDataKey to IAM role | | Retention not enforced | Cron not running | Schedule retention job, add monitoring | | Erasure incomplete | Transaction failed | Use database transactions for atomic delete |
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-19 | pass→pass | 16,672 | 13,281 | -20% | 1 | 1 | 0% | 2,768 | 5,169 | +87% | 0 | 0 | — |
case-01 | fail→pass | 11,897 | 10,973 | -8% | 1 | 1 | 0% | 2,950 | 5,349 | +81% | 0 | 0 | — |
case-02 | pass→pass | 8,571 | 4,800 | -44% | 1 | 1 | 0% | 1,585 | 3,760 | +137% | 0 | 0 | — |
case-03 | pass→pass | 6,207 | 5,204 | -16% | 1 | 1 | 0% | 1,259 | 3,829 | +204% | 0 | 0 | — |
case-04 | fail→pass | 10,142 | 8,102 | -20% | 1 | 1 | 0% | 1,931 | 4,459 | +131% | 0 | 0 | — |
case-05 | fail→pass | 12,191 | 7,387 | -39% | 1 | 1 | 0% | 2,307 | 4,184 | +81% | 0 | 0 | — |
case-06 | fail→pass | 13,831 | 7,185 | -48% | 1 | 1 | 0% | 2,560 | 4,138 | +62% | 0 | 0 | — |
case-07 | fail→pass | 8,056 | 2,398 | -70% | 1 | 1 | 0% | 1,421 | 3,207 | +126% | 0 | 0 | — |
case-08 | pass→pass | 14,173 | 7,358 | -48% | 1 | 1 | 0% | 2,831 | 4,121 | +46% | 0 | 0 | — |
case-09 | fail→fail | 9,375 | 7,082 | -24% | 1 | 1 | 0% | 1,571 | 4,038 | +157% | 0 | 0 | — |
case-10 | fail→fail | 15,413 | 12,450 | -19% | 1 | 1 | 0% | 2,862 | 5,127 | +79% | 0 | 0 | — |
case-11 | fail→fail | 10,032 | 7,543 | -25% | 1 | 1 | 0% | 2,080 | 4,239 | +104% | 0 | 0 | — |
case-12 | pass→pass | 10,317 | 8,358 | -19% | 1 | 1 | 0% | 2,074 | 4,362 | +110% | 0 | 0 | — |
case-13 | fail→pass | 9,209 | 1,935 | -79% | 1 | 1 | 0% | 1,567 | 3,047 | +94% | 0 | 0 | — |
case-14 | fail→pass | 8,032 | 2,074 | -74% | 1 | 1 | 0% | 1,296 | 3,047 | +135% | 0 | 0 | — |
case-15 | fail→pass | 10,879 | 1,148 | -89% | 1 | 1 | 0% | 1,682 | 2,912 | +73% | 0 | 0 | — |
case-16 | pass→pass | 11,540 | 10,176 | -12% | 1 | 1 | 0% | 1,810 | 4,697 | +160% | 0 | 0 | — |
case-17 | pass→pass | 10,135 | 2,950 | -71% | 1 | 1 | 0% | 1,964 | 3,319 | +69% | 0 | 0 | — |
case-18 | fail→pass | 19,526 | 16,674 | -15% | 1 | 1 | 0% | 3,310 | 5,875 | +77% | 0 | 0 | — |
case-20 | fail→fail | 6,618 | 5,090 | -23% | 1 | 1 | 0% | 1,105 | 3,538 | +220% | 0 | 0 | — |
case-21 | pass→pass | 15,026 | 15,788 | +5% | 1 | 1 | 0% | 3,102 | 6,277 | +102% | 0 | 0 | — |
case-22 | pass→pass | 4,831 | 5,663 | +17% | 1 | 1 | 0% | 989 | 3,899 | +294% | 0 | 0 | — |
case-23 | pass→pass | 11,420 | 10,614 | -7% | 1 | 1 | 0% | 2,443 | 5,008 | +105% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 23 cases were attempted. The headline lift of +39 percentage points is the difference between those two pass rates over the 23 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.