Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

33 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Voice Message Transcriber

Chrome extension that transcribes voice messages on WhatsApp Web, Telegram Web and Discord — entirely in the browser, with no audio sent anywhere.

Transcription runs locally using Whisper via transformers.js and ONNX Runtime — on the GPU through WebGPU where available, falling back to WebAssembly otherwise. No API key or account required.


Supported platforms

Platform URL
WhatsApp Web web.whatsapp.com
Telegram Web web.telegram.org
Discord discord.com

Features

  • 100% local — no API key, no server, no audio sent anywhere
  • On-demand — only active while the floating panel is open; dormant otherwise
  • Cached — the same audio is never transcribed twice (SHA-256 hash stored in chrome.storage.local)
  • Four Whisper models — Tiny (~41 MB), Base (~77 MB), Small (~325 MB, recommended), Large v3 Turbo (~759 MB)
  • Live transcript — text appears as the model decodes it, not once the whole clip is done
  • Silence trimming — non-speech is cut before inference: faster, and fewer hallucinations
  • 14 languages — select your language once in settings
  • Model management — download and delete models directly from the settings panel
  • Visual Queue — placeholder bubbles indicate which queued voice messages are waiting to be processed
  • Pause — keep the panel open but temporarily stop transcribing new messages
  • Silent Mode — automatically mute the voice message while transcribing
  • Export — save all transcriptions to a text file (in settings)
  • Timestamps — optional [MM:SS] markers in the text
  • Floating panel — draggable, persistent across navigation, close and reopen any time
  • Copy — click any entry to copy it, or use "Copy all"
  • Stop — cancel an in-progress transcription without unloading the model from memory

Requirements

  • Chrome 112 or later (Manifest V3 + Offscreen Documents API)
  • Node.js 18 or later (build only)

Installation

git clone https://github.com/RobertoReale/Voice-Message-Transcriber.git
cd whatsapp-web-transcriber
npm install
npm run build

Then load the extension in Chrome:

  1. Go to chrome://extensions/
  2. Enable Developer mode (top-right toggle)
  3. Click Load unpacked → select the dist/ folder

Usage

  1. Open any supported platform
  2. Click the extension icon (top-right of Chrome) to open the transcription panel
  3. First run: open ⚙ Settings, select your language, then choose a model — it downloads automatically (~325 MB for the recommended Whisper Small, ~77 MB for Base if you are on a metered connection)
  4. Keep the panel open and play any voice message — the transcription appears in the panel
  5. Click any entry to copy it to the clipboard

Note: The extension is dormant while the panel is closed — no audio is intercepted or transcribed.

Panel buttons

Button Action
⚙️ Settings — language, model, download, export, timestamps and more
⏸️/▶️ Pause/Resume transcription (keeps panel open but stops processing new messages)
📋 Copy all transcriptions to clipboard
🔊/🔇 Toggle Silent Mode (mutes the audio of the voice message when transcribing)
🗑 Clear the visible list (cache preserved — same audio won't re-transcribe)
Stop the current in-progress transcription
Hide panel (reopen from extension icon)

Export and cache controls are in ⚙ Settings.


Architecture

src/
├── injected/interceptor.ts       — HTMLMediaElement.src patch (world: MAIN, document_start)
├── content/index.ts              — fetch, OGG/Opus decode, cache check, port to SW
├── background/service-worker.ts — port router, offscreen lifecycle, model cache reconciliation
├── offscreen/whisper.ts          — Whisper pipeline (transformers.js + ONNX WebGPU/WASM)
└── ui/                           — floating panel, settings, styles, drag

Audio interception

injected/interceptor.ts runs in the page context (world: "MAIN") at document_start, before any site JS. It patches HTMLMediaElement.prototype.src. Two patterns are captured:

  • Blob URLs (blob:https://...): WhatsApp Web and Telegram Web download audio to memory before playing it, producing a blob URL.
  • Discord CDN URLs (cdn.discordapp.com/.../voice-message*): Discord voice messages are served directly from the CDN as signed HTTPS URLs.

The manifest restricts which origins the script is injected on, so the patch is a no-op on every other site.

Message flow

Site assigns audio URL (blob or CDN)
  → injected script (.src setter fires → postMessage to content script)
    → content script
        fetch(url)  →  ArrayBuffer
        AudioContext.decodeAudioData()  →  16 kHz Float32 PCM
        SHA-256 hash  →  check chrome.storage.local
        [cache hit]  →  display immediately
        [cache miss] →  open port to service worker
          → service worker
              chrome.runtime.sendMessage(WHISPER_TRANSCRIBE)
              → offscreen document
                  load / reuse Whisper model (ONNX WebGPU, WASM fallback)
                  run inference
              ← text
            store in chrome.storage.local
          ← TRANSCRIBE_RESULT via port
      display in panel

OGG/Opus decoding happens in the content script (not the offscreen document) because Chrome MV3 offscreen documents lack the codec in AudioContext.

The content-script ↔ service-worker channel is a long-lived port (chrome.runtime.connect), which keeps the MV3 service worker alive for the entire duration of the transcription.

Models

Models are fetched from Hugging Face via transformers.js and stored in the browser's Cache API (caches.open('transformers-cache')). After the first download no network request is made.

Model Encoder Decoder Size Device Notes
onnx-community/whisper-tiny q8 q8 ~41 MB WASM fastest · lowest accuracy
onnx-community/whisper-base q8 q8 ~77 MB WASM fast · light
onnx-community/whisper-small q8 q4 ~325 MB WebGPU default · recommended
onnx-community/whisper-large-v3-turbo q4 q4 ~759 MB WebGPU most accurate · slowest

Sizes are encoder_model + decoder_model_merged at the dtypes above — the only two ONNX files transformers.js loads for Whisper — and are derived in code from sizeMb so the panel can never quote a figure the build does not actually download.

Quantization is per module, not per model. The transformers.js documentation is explicit that Whisper is "extremely sensitive to quantization settings, especially of the encoder", so the encoder never goes below int8 (q8). The exception is Large v3 Turbo, whose int8 encoder is 645 MB against 425 MB for q4. Decoders are q4 where the model runs on WebGPU, which serves MatMulNBits well, and q8 on WASM, where the int8 kernels are the mature path.

Note that on the small models 4-bit is larger than 8-bit: MatMulNBits carries scales and zero-points and leaves the embedding tables in fp32, which dominate at that scale. Versions ≤1.2.1 used a flat q4 and shipped Whisper Tiny as 96 MB behind a "~40 MB" label; the same model at q8 really is 41 MB.

Small and Large v3 Turbo run on the GPU through WebGPU, with automatic fallback to WASM if the adapter is unavailable or the GPU pipeline throws. Tiny and Base stay on WASM: they are genuinely faster there, because at that model size the GPU dispatch overhead costs more than the compute it saves. WASM inference uses up to 4 threads when SharedArrayBuffer is available.

WebGPU used to be disabled for all q4 models, because ONNX Runtime's WebGPU backend returned [Music] instead of speech on them. Re-measured on 2026-08-04 against the pinned dependency stack, which has not changed since the first commit: output is character-identical to WASM on all three models, on real voice messages, and WebGPU is 1.5x faster on Small and 2.8x on Large v3 Turbo. The failure does not reproduce on any ONNX Runtime version from 1.21.0-dev (Oct 2024) to 1.26.0-dev, nor on Chrome 135 through 150 — see docs/webgpu-onnxruntime-fix.md for the method, the numbers and what was ruled out. A q8 decoder stays off the GPU: that is where the old untested "Can't perform where op" crash came from, Where being part of the decoder's attention masking rather than anything in the encoder.

Changing a dtype changes the ONNX filename, so the weights fetched by a previous version would otherwise sit in the Cache API forever. On update the service worker evicts every cached .onnx a model no longer loads, and marks that model as needing a re-download.

Key inference settings that reduce hallucinations and repetition loops:

  • num_beams: 1, temperature: 0 — fast deterministic decoding
  • repetition_penalty: 1.1 — discourages repeating tokens
  • condition_on_prev_tokens: false — prevents chunk-boundary repetition
  • compression_ratio_threshold: 2.4, no_speech_threshold: 0.5 — discard non-speech chunks
  • Post-processing: consecutive identical segments are stripped before display

Silence trimming

Silence is what Whisper hallucinates on — the [Music] and "Subtitles by…" that isNonSpeech has to filter out after the fact. src/shared/vad.ts cuts the non-speech regions before inference instead, which also shortens the clip the model has to process.

It is an energy detector with hysteresis, not a neural VAD: it borrows Silero's framing (32 ms frames at 16 kHz) and its two-threshold structure, but decides on frame RMS against a noise floor estimated from the clip itself. detectSpeechRegions returns null — meaning "use the audio unchanged" — whenever the clip cannot be segmented confidently: too short, too little contrast between speech and background, or no speech found. A trim is only accepted if it saves at least a second and leaves at least half a second behind, because a false negative here deletes someone's words. Trimming is skipped entirely when timestamps are enabled, since removing audio would shift every timestamp away from the recording.

Warm-up and live text

The first inference after a model load pays for shader compilation on WebGPU and the first pass through the graph on WASM. A throwaway one-token inference is queued right after the load so the user's first voice message does not pay it.

While decoding, a WhisperTextStreamer forwards the text as it is produced (TRANSCRIPTION_PARTIAL, coalesced to one message per 150 ms) and the panel fills the pending bubble in progressively. The displayed result still comes from the pipeline's own output — the stream only drives the preview, and on chunked audio it can repeat a word across a window seam.


Privacy

  • No audio leaves the browser
  • No telemetry or analytics
  • No external network requests after the one-time model download from Hugging Face
  • Transcription cache stored in chrome.storage.local, clearable from the panel at any time

Roadmap / Future Ideas

  • Summarization: Use a local SLM (Small Language Model) via WebGPU to summarize very long voice messages into bullet points.
  • Built-in Translation: Use Whisper's native task: "translate" to automatically translate foreign language voice messages to English (or your native language) on the fly.
  • Speaker Diarization: Separate and label text by different speakers (e.g., "Speaker 1", "Speaker 2") for group audio or calls.

Development

npm run build   # production build → dist/
npm run dev     # same as build (reload extension manually in chrome://extensions after each build)
npm run lint    # TypeScript type check (tsc --noEmit)
npm test        # unit tests (node:test, TypeScript run directly)

License

MIT


Credits

About

Chrome extension that transcribes voice messages on WhatsApp Web, Telegram Web and Discord — entirely in the browser, with no audio sent anywhere. Transcription runs locally using Whisper via transformers.js and ONNX Runtime, on the GPU through WebGPU where available and on WebAssembly otherwise. No API key or account required.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages