| name | speak-data-handling |
| description | Implement Speak PII handling, audio data retention, and GDPR/CCPA compliance patterns.
Use when handling user learning data, implementing audio retention policies,
or ensuring privacy compliance for language learning applications.
Trigger with phrases like "speak data", "speak PII",
"speak GDPR", "speak data retention", "speak privacy", "speak audio privacy".
|
| allowed-tools | Read, Write, Edit |
| version | 1.0.0 |
| license | MIT |
| author | Jeremy Longshore <jeremy@intentsolutions.io> |
Speak Data Handling
Overview
Handle sensitive user data and audio recordings correctly when integrating with Speak language learning.
Prerequisites
- Understanding of GDPR/CCPA requirements
- Speak SDK with data export capabilities
- Database for audit logging
- Scheduled job infrastructure for cleanup
- Audio storage with encryption
Data Classification
| Category | Examples | Handling |
|---|
| PII | Email, name, phone | Encrypt, minimize |
| Sensitive | API keys, tokens | Never log, rotate |
| Learning Data | Scores, progress | Anonymize for analytics |
| Audio Recordings | Voice samples | Encrypt, consent required |
| User Preferences | Languages, goals | Standard handling |
Audio Data Privacy
Audio Consent Management
interface AudioConsent {
userId: string;
consentGiven: boolean;
consentDate: Date;
purposes: ('pronunciation_scoring' | 'model_improvement' | 'playback')[];
retentionDays: number;
canWithdraw: boolean;
}
class AudioConsentManager {
async getConsent(userId: string): Promise<AudioConsent | null> {
return db.audioConsents.findOne({ userId });
}
async grantConsent(
userId: string,
purposes: AudioConsent['purposes'],
retentionDays: number = 30
): Promise<void> {
await db.audioConsents.upsert({
userId,
consentGiven: true,
consentDate: new Date(),
purposes,
retentionDays,
canWithdraw: true,
});
await ({
: ,
userId,
purposes,
retentionDays,
});
}
(: ): <> {
db..(userId, {
: ,
: (),
});
.(userId);
({
: ,
userId,
});
}
(: ): <> {
consent = .(userId);
consent?. === &&
consent..();
}
}
Secure Audio Storage
class SecureAudioStorage {
private encryptionKey: Buffer;
private storage: StorageBackend;
constructor(encryptionKeyBase64: string, storage: StorageBackend) {
this.encryptionKey = Buffer.from(encryptionKeyBase64, 'base64');
this.storage = storage;
}
async storeAudio(
userId: string,
sessionId: string,
audioData: ArrayBuffer,
metadata: AudioMetadata
): Promise<string> {
const encrypted = await this.encrypt(audioData);
const audioId = crypto.randomUUID();
const consent = await audioConsentManager.getConsent(userId);
const expiryDate = new Date();
expiryDate.setDate(expiryDate.() + (consent?. || ));
..(, encrypted, {
: {
: (userId),
sessionId,
: metadata.,
: metadata.,
: ().(),
: expiryDate.(),
},
});
db..({
audioId,
: (userId),
: expiryDate,
});
({
: ,
userId,
audioId,
sessionId,
: expiryDate,
});
audioId;
}
(: ): <> {
mappings = db..({
: (userId),
});
( mapping mappings) {
..();
db..(mapping.);
}
({
: ,
userId,
: mappings.,
});
mappings.;
}
(: ): <> {
iv = crypto.( ());
key = crypto..(
,
.,
,
,
[]
);
encrypted = crypto..(
{ : , iv },
key,
data
);
result = (iv. + encrypted.);
result.(iv);
result.( (encrypted), iv.);
result.;
}
}
Learning Data Handling
PII Detection in Learning Data
const PII_PATTERNS = [
{ type: 'email', regex: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g },
{ type: 'phone', regex: /\b\d{3}[-.]?\d{3}[-.]?\d{4}\b/g },
{ type: 'name_pattern', regex: /my name is ([A-Z][a-z]+ ?)+/gi },
{ type: 'address', regex: /\d+\s+[\w\s]+(?:street|st|avenue|ave|road|rd|blvd)/gi },
];
function detectPIIInLessonContent(text: string): PIIFinding[] {
const findings: PIIFinding[] = [];
for (const pattern of PII_PATTERNS) {
const matches = text.matchAll(pattern.regex);
for (const match of matches) {
findings.push({
type: pattern.type,
match: match[0],
position: match.index,
});
}
}
return findings;
}
async function sanitizeLessonResponse(
:
): <> {
findings = (response.);
(findings. > ) {
.(, {
: findings.( f.),
});
sanitizedText = response.;
( finding findings) {
sanitizedText = sanitizedText.(finding., );
}
{ ...response, : sanitizedText };
}
response;
}
Data Retention Policy
interface RetentionPolicy {
dataType: string;
retentionDays: number;
reason: string;
}
const RETENTION_POLICIES: RetentionPolicy[] = [
{ dataType: 'audio_recordings', retentionDays: 30, reason: 'User consent period' },
{ dataType: 'lesson_transcripts', retentionDays: 90, reason: 'Learning history' },
{ dataType: 'pronunciation_scores', retentionDays: 365, reason: 'Progress tracking' },
{ dataType: 'session_logs', retentionDays: 30, reason: 'Debugging' },
{ dataType: 'error_logs', retentionDays: 90, reason: 'Root cause analysis' },
{ dataType: 'audit_logs', retentionDays: 2555, reason: 'Compliance (7 years)' },
{ dataType: 'user_preferences', retentionDays: -1, reason: 'Until account deletion' },
];
(): <> {
: = { : {} };
( policy ) {
(policy. < ) ;
cutoff = ();
cutoff.(cutoff.() - policy.);
count = db[policy.].({
: { : cutoff },
});
report.[policy.] = count;
}
({
: ,
report,
});
report;
}
cron.(, cleanupExpiredData);
GDPR/CCPA Compliance
Data Subject Access Request (DSAR)
interface UserDataExport {
exportedAt: string;
userId: string;
profile: UserProfile;
learningData: {
languages: string[];
totalLessons: number;
totalPracticeTime: number;
pronunciationHistory: PronunciationRecord[];
vocabularyProgress: VocabularyProgress[];
streakHistory: StreakRecord[];
};
audioRecordings: {
count: number;
totalDuration: number;
files?: ArrayBuffer[];
};
consents: ConsentRecord[];
auditTrail: AuditEntry[];
}
async function exportUserData(
userId: string,
includeAudio: boolean = false
): Promise<UserDataExport> {
const [
profile,
lessons,
pronunciation,
vocabulary,
streaks,
audioMeta,
consents,
audit,
] = await Promise.all([
db.users.({ : userId }),
db..({ userId }),
db..({ userId }),
db..({ userId }),
db..({ userId }),
db..({ : (userId) }),
db..({ userId }),
db..({ userId }),
]);
: [] | ;
(includeAudio && audioMeta. > ) {
audioFiles = .(
audioMeta.( audioStorage.(meta.))
);
}
: = {
: ().(),
userId,
: (profile),
: {
: (lessons),
: lessons.,
: (lessons),
: pronunciation,
: vocabulary,
: streaks,
},
: {
: audioMeta.,
: audioMeta.( sum + m., ),
: audioFiles,
},
consents,
: audit,
};
({
: ,
userId,
includeAudio,
: (),
});
exportData;
}
Right to Deletion
interface DeletionResult {
success: boolean;
deletedItems: Record<string, number>;
retainedForCompliance: string[];
deletedAt: Date;
}
async function deleteUserData(userId: string): Promise<DeletionResult> {
const deletedItems: Record<string, number> = {};
const retainedForCompliance: string[] = [];
try {
await speakClient.users.delete(userId);
deletedItems['speak_remote'] = 1;
} catch (error) {
console.error('Failed to delete from Speak:', error);
}
deletedItems['lessons'] = await db.lessons.deleteMany({ userId });
deletedItems['pronunciation'] = await db.pronunciationScores.deleteMany({ userId });
deletedItems['vocabulary'] = db..({ userId });
deletedItems[] = db..({ userId });
deletedItems[] = db..({ userId });
deletedItems[] = audioStorage.(userId);
db..(
{ userId },
{ : { : , : } }
);
retainedForCompliance.();
({
: ,
: ,
: (userId),
deletedItems,
retainedForCompliance,
: (),
});
deletedItems[] = db..({ : userId });
{
: ,
deletedItems,
retainedForCompliance,
: (),
};
}
Output
- Audio consent management
- Secure audio storage with encryption
- PII detection and sanitization
- Retention policy enforcement
- GDPR/CCPA compliance (export/delete)
Error Handling
| Issue | Cause | Solution |
|---|
| PII in lessons | User shared personal info | Sanitize before storage |
| Audio not deleted | Storage error | Retry with exponential backoff |
| Export incomplete | Timeout | Use streaming export |
| Consent not recorded | Race condition | Use transactions |
Examples
Quick Data Privacy Check
async function privacyStatusCheck(userId: string): Promise<PrivacyStatus> {
const consent = await audioConsentManager.getConsent(userId);
const audioCount = await db.audioMappings.count({ userId: encrypt(userId) });
return {
hasAudioConsent: consent?.consentGiven ?? false,
audioRecordingsStored: audioCount,
nextAudioExpiry: await getNextAudioExpiry(userId),
dataExportAvailable: true,
deletionAvailable: true,
};
}
Resources
Next Steps
For enterprise access control, see speak-enterprise-rbac.