Technical paper
A walkthrough of the design, reasoning and technical choices behind a browser-based privacy filter for voice assistant transcripts.
VoiceSafeKit is a privacy filter that sits between a voice assistant and a large language model. It records audio in the browser, transcribes it with a Whisper speech model running on the user's device, then checks the text for sensitive information before it can be sent to an AI. The project is a static web app with a mirrored Python package for CLI and server-side use. This paper explains the problem it solves, how it was designed, what it detects and why those choices were made.
01 - The Problem
Voice assistants are becoming more capable because they can now pass what a user says directly to a large language model. That creates a real risk. When someone speaks to an assistant, they often include private details without thinking: an email address, a phone number, a password reset flow, a medical question. The assistant records all of it and sends it upstream.
There is usually nothing in between. The raw transcript goes to a cloud model, often unfiltered. Most developers building voice assistants focus on intent detection and response quality. Privacy filtering at the speech input layer is rarely a priority and off-the-shelf solutions for it are almost non-existent.
VoiceSafeKit addresses that gap. It is a safety layer that checks what was said before it leaves the device.
A user says: "My email is john@example.com and my password is BlueFalcon2026. Help me reset my account." Without filtering, that entire string goes to the model. VoiceSafeKit flags the email and password, produces a redacted version and tells the assistant not to echo the credentials back.
02 - Architecture
The system is split into three clearly separated stages. Each one has a single responsibility.
STAGE 01
Record audio via the browser microphone, transcribe it locally using Whisper, or accept pasted text.
STAGE 02
Run the transcript through a set of pattern rules. Score the result. Assign a decision label.
STAGE 03
Produce a redacted transcript and plain-English guidance for the assistant to follow.
The data flow within Stage 1 is worth showing explicitly:
Nothing in that chain touches a server. The audio is captured in the browser, decoded in the browser and transcribed in the browser. The only thing that ever leaves the device is the cleaned transcript and only if the user chooses to forward it.
03 - In-Browser Speech Recognition
The first version of VoiceSafeKit used the browser's built-in Web Speech API. It was unreliable. The API stops recognising speech after a short pause, varies widely between browsers and in some Chrome versions silently fails unless a separate microphone permission call has already been made. It is also inherently online: Chrome's implementation sends audio to Google's servers.
The replacement is Whisper, a speech recognition model published by OpenAI in 2022 and widely regarded as one of the most accurate open speech models available. The variant used here is whisper-tiny.en, an English-only model around 75 MB in size.
Transformers.js (by Hugging Face) compiles the model to ONNX format and runs it via WebAssembly. No GPU is needed. The model downloads once and is then stored permanently in the browser's IndexedDB cache. Every subsequent run is instant.
The transcription pipeline, simplified:
// Decode the audio blob to 16kHz mono - Whisper's required format const audioCtx = new AudioContext({ sampleRate: 16000 }); const decoded = await audioCtx.decodeAudioData(arrayBuffer); const float32 = decoded.getChannelData(0); // Run Whisper const model = await pipeline("automatic-speech-recognition", "Xenova/whisper-tiny.en"); const result = await model(float32, { sampling_rate: 16000 }); const text = result.text.trim();
The model is loaded lazily: it starts downloading as soon as the user presses Record, so by the time they finish speaking, it is usually ready. On repeat visits, the cached version loads in under a second.
Why tiny over base? The tiny model (75MB) completes transcription in roughly 2-4 seconds on a typical laptop. The base model (145MB) is more accurate but takes longer to load and process. For a tool where speed matters for usability, tiny is the right trade-off.
04 - The Detection Engine
Once the transcript exists as plain text, VoiceSafeKit runs it through a set of detection rules. Each rule has a pattern (a regular expression), a severity level, a human-readable explanation and a replacement string for the redacted output.
Medium
Standard RFC-compliant email pattern.
Medium
International and local formats. Rejects 13+ digit sequences to avoid clashing with card numbers.
High
13-19 digit groups with optional spaces or dashes.
High
Keyword-triggered: "password is", "token:", "api key =".
Medium
Number followed by street name suffix.
High
Keywords: chest pain, medication, dose, blood pressure.
High
Keywords: lawsuit, eviction, immigration, legal advice.
High
Keywords: crypto, credit score, loan application, mortgage.
Critical
Keywords: can't breathe, heart attack, overdose, hurt myself.
Every finding contributes to a risk score. The score is additive, capped at 100.
| Level | Points | Examples |
|---|---|---|
| LOW | 10 | Reserved for future rules |
| MEDIUM | 22 | Email, phone number, street address |
| HIGH | 36 | Password, card number, medical / legal / financial intent |
| CRITICAL | 55 | Emergency language, immediate harm |
SAFE
SAFE
Score: 0-21
No concerning content found. The assistant can proceed normally.
REDACT
REDACT
Score: 22-54
Low-risk personal data found. Use the cleaned version instead.
REVIEW
REVIEW
Score: 55+ or any HIGH
Sensitive content found. A human or policy check is advised before forwarding.
BLOCK
BLOCK
Any CRITICAL finding
Emergency or immediate harm detected. Do not forward. Route to emergency help.
When two rules match overlapping text (for example, a 16-digit number that could be both a phone and a card), the higher-severity rule wins. The overlapping lower-severity finding is discarded. This prevents double-counting and ensures the more serious label is shown.
function removeOverlaps(findings) { const accepted = []; findings.sort((a, b) => severityRank[b.severity] - severityRank[a.severity] ); for (const f of findings) { if (!accepted.some(x => f.start < x.end && x.start < f.end)) accepted.push(f); } return accepted; }
05 - Technical Stack
HTML / CSS / JS
Zero framework, zero build step. Runs as static files on GitHub Pages.
@xenova/transformers
Runs Whisper in the browser via ONNX + WebAssembly.
MediaRecorder API
Browser-native audio recording. No extra library needed.
AudioContext API
Decodes the audio blob to 16kHz mono PCM for Whisper.
Web Crypto API
Encrypts browser exports before they are saved as JSON files.
GitHub Pages
HTTPS static hosting. Required for microphone access permissions.
Python package
The same detection rules mirrored in Python for CLI or server-side use.
cryptography
Adds AES-GCM encrypted exports to the Python CLI.
pytest
Unit tests for the Python detection engine.
ruff
Python linter. Enforced in CI.
Using React, Vue or any other framework would require a build step, a node_modules folder and a bundler. The point of this project is that it should run as static files on any HTTPS host with no server, no backend and no deployment complexity. Vanilla HTML/JS keeps that simple.
06 - Design Decisions
Why no API key for transcription? Any API key embedded in a public GitHub repository is public. Anyone can read your source, extract the key and use it. Running the model entirely in the browser avoids this problem entirely. It also means the audio never touches a third-party server, which is directly relevant to the privacy use case.
Why not use the Luhn algorithm to validate card numbers? The Luhn checksum is used by card issuers to catch typos. It does not distinguish real cards from made-up ones that happen to pass. More importantly, a user dictating a card number to a voice assistant may speak the digits in a way that produces a transcript with a typo. The card would fail Luhn but should still be flagged. A safety tool should err on the side of flagging too much, not too little.
Why flag intent (medical, legal, financial) separately from data? A transcript might not contain any personally identifiable information but still be problematic. Someone asking an AI for a medication dose change or investment advice needs different handling to someone asking for a restaurant recommendation. Intent rules capture that distinction.
Why client-side everything? The thesis of the project is that sensitive voice data should not leave the device unless it has been checked and cleaned. If the checking itself required sending data to a server, the whole premise would collapse. Everything runs locally by design.
Why encrypt saved exports? The filter has to read plain text while it is checking the transcript. That part happens in memory. Saved files are different. They can include the redaction map, which may contain the exact email address, SSN or account number that was removed from the safe transcript. Browser exports now use the Web Crypto API to save encrypted JSON. The Python CLI uses AES-256-GCM through the cryptography package. TLS/HTTPS is still useful when data travels over a network, but it does not encrypt a file sitting on disk.
How the export encryption was built. The analysis engine was left unchanged: it still produces the same structured result with decision, findings, safe_transcript and redaction_map. The encryption layer sits after analysis, at the moment a user chooses to save the result. That keeps detection simple while making the saved artifact safer.
In the browser, the Export JSON button requires a local export key, derives an AES-GCM key through Web Crypto and saves only the encrypted bundle. In the CLI, --out writes encrypted files by default using VOICESAFEKIT_EXPORT_KEY. The explicit --plain-out option is kept for local debugging, and voicesafekit decrypt restores an encrypted export when the same passphrase is available. Tests cover the round trip and confirm that sensitive redaction-map values are not visible in the stored JSON.
07 - Limitations
Accent and noise sensitivity. Whisper tiny is accurate for clear English speech in a quiet environment. Heavy accents, background noise, or fast speech can produce transcription errors. Those errors may cause sensitive content to be missed. The base model would improve accuracy but at the cost of speed.
Keyword rules miss novel phrasing. The detection rules use fixed patterns and keywords. Someone saying "give me a new dose" rather than "change my medication" would not trigger the medical rule. A neural classifier trained on intent would handle this better.
No streaming transcription. The current design records a full clip and then transcribes it. Real-time word-by-word transcription is possible with a sliding-window approach to Whisper, but it was not implemented here.
Not a security product. VoiceSafeKit is a demonstration of the concept. It should not be used as the sole privacy control in a production voice assistant. It is a starting point and a reference implementation.
08 - Future Work
The most natural next step is integration with real open-source voice assistant frameworks. Two candidates are already referenced in the project:
OpenVoiceOS is an open-source voice operating system built on the Mycroft platform. A VoiceSafeKit adapter would intercept the STT output before it is passed to the intent parser, giving privacy filtering at the right layer of the pipeline.
Wyoming protocol is a lightweight protocol used by Home Assistant for communicating between voice satellite devices and processing servers. A Wyoming microservice wrapping the VoiceSafeKit detection engine would allow any Wyoming-compatible assistant to use it as a filter stage.
Other areas worth exploring include streaming transcription, neural intent classification to replace keyword rules and a dashboard for assistant developers to tune sensitivity thresholds per deployment context.
09 - Confidence Scoring
The original engine returned binary findings: a pattern either matched or it did not. That approach overstates certainty. A phone number matched near a credit card number is less likely to be a real phone number than one found in isolation. A medical keyword in a clearly technical context is less alarming than the same keyword in an emotionally charged message. Binary matching discards this context entirely.
Version 0.2.0 adds a confidence: float field to every Finding. Each rule declares a base confidence that reflects how precise its pattern is. Context-aware validators then adjust it upward or downward.
The risk score is now computed as:
def _score(findings: list[Finding]) -> int: total = sum(SEVERITY_POINTS[f.severity] * f.confidence for f in findings) return min(100, round(total))
A 70%-confidence high-severity finding contributes 25 points (36 × 0.70) rather than the full 36. A Luhn-verified payment card at 99% confidence contributes 35.6 points. This means the score reflects genuine risk rather than a count of regex matches.
| Detector | Base confidence | Reason |
|---|---|---|
| Email address | 98% | Pattern is highly specific |
| Social Security Number | 95% | Pattern + invalid-range filter |
| IBAN / Bank account | 88% | Pattern + mod-97 checksum |
| Phone number | 85% | Prone to false positives from digit sequences |
| Emotional distress | 70% | Nuanced language, intent is ambiguous |
| Coercion signal | 65% | Hardest to detect reliably from text alone |
10 - Multi-Turn Conversation Analysis
Voice assistants do not work in isolated utterances. A user might give their name in the first turn, their email address in the second and their date of birth to confirm their identity in the third. Analyzed individually, turns two and three each get a medium-severity result. Seen together, the session reveals a structured identity disclosure that carries much higher real-world risk.
The analyze_conversation function takes an ordered list of utterances and returns a ConversationResult with per-turn analysis, a peak decision and the union of all entity kinds detected across the session.
from voicesafekit import analyze_conversation result = analyze_conversation([ "Set a timer for ten minutes.", "My SSN is 471-55-8843 - please verify me.", "Thank you, that is all.", ]) result.peak_decision # "BLOCK" result.cumulative_score # 95 result.seen_kinds # ("ssn",) len(result.turns) # 3
The web app exposes this as a Conversation mode toggle. Each turn is added explicitly, a turn history panel tracks the sequence and a timeline view below the main panels shows every turn's findings and decision alongside a cumulative summary bar.
Cross-turn entity tracking (noticing that "my card" in turn 3 refers to the card number mentioned in turn 1) is not yet implemented. Each turn is currently analyzed in isolation. This is noted as a limitation and is the most important remaining engineering gap.
11 - New Detector Classes (June 2026)
The v0.2.0 detector set adds three categories that were entirely absent from the original engine.
SSNs, IBANs and CVV codes are the three most common financial identifiers that a voice assistant user might accidentally speak. Each has a distinct detection challenge.
SSN - The pattern \b(?!000|666|9\d{2})\d{3}[-\s]\d{2}[-\s]\d{4}\b excludes area numbers that the Social Security Administration has never issued. This eliminates a class of false positives from account numbers and tracking codes that happen to match the XXX-XX-XXXX format.
IBAN - The pattern matches the standard two-letter country code, two check digits and 11-30 alphanumeric BBAN characters. A mod-97 checksum then validates the matched text. Without this step, any sufficiently long alphanumeric string near a country code would be flagged.
CVV - Unlike card numbers, CVV codes are short (3-4 digits) and indistinguishable from any other number without context. The detector is keyword-triggered: it only fires when a CVV/CVC/security-code phrase precedes the number.
Two new intent categories address situations that no PII detector can catch but that require fundamentally different assistant behavior.
The emotional distress signal fires on phrases expressing hopelessness, worthlessness, or a desire to disappear. When detected, the assistant guidance changes: instead of "redact and proceed," the guidance tells the assistant to respond with empathy and direct the user to appropriate support. The confidence is set at 70% because emotional language is genuinely ambiguous - "I feel like giving up" can mean quitting a project as easily as it can signal a crisis.
The coercion signal fires on phrases suggesting the user is being directed by a third party: "they made me say this," "don't tell anyone I said this." This is the lowest-confidence detector in the system (65%) because coercive language and ordinary reporting language overlap substantially. A higher false negative rate is acceptable here because the guidance produced is non-destructive - it simply tells the assistant not to act on instructions that may have been forced.
12 - Closing
Voice assistants are getting smarter quickly. The gap between what someone says and what an AI model is allowed to act on needs careful thought. Most of the safety work in this space focuses on model outputs (preventing harmful responses) rather than model inputs (preventing harmful data going in).
VoiceSafeKit is an argument that input filtering matters too and that it can be done locally, in a browser, with no infrastructure and with a good user experience. The code is open-source. The approach is intentionally simple so that it can be understood, copied and improved.
The live application is at mohamadkanso.github.io/VoiceSafeKit and the full source is on GitHub.