FASTER-WHISPER INT8 · CPU-ONLY · 100% PRIVATE

Your WhatsApp voice notes, as text — no cloud.

Voice notes are the dominant format on WhatsApp (especially in LATAM) — and they're annoying to listen to in a meeting or in a rush. Cloud STT services receive your audio; WhatsApp's own transcription is mediocre. This pipeline transcribes every incoming voice note — and the ones you send yourself — with faster-whisper running locally on a CPU-only node, and the text comes back as a normal message. It started as a personal tool; today it is a shared transcription engine several engineers use daily, with other services hanging off the same endpoint. The audio never leaves the network.

3.2x
wall clock per voice note: 34 s to 10 s, same hardware
2 nodes
one captures, one transcribes · both always-on, both small
0 GPU
the GPU left the design entirely - it was never the bottleneck
0 cloud STT
the only thing that leaves is the final text, via WhatsApp
Why two nodes

The cost was fixed, not proportional.

A 3-second voice note took as long as a 30-second one. The engine pads every clip to a fixed 30-second window, so short notes - which is almost all of them - paid for silence. The fix was not a faster GPU: it was an int8 runtime that uses the AVX-512 VNNI instructions already sitting in the CPU, plus voice-activity detection so silence is never transcribed at all. Measured across three durations: 7.1x, 6.9x and 6.0x at the engine, 3.2x end to end once model load and format conversion are counted. Both numbers are here because they are not the same number.

🖥️ CAPTURE NODE · ALWAYS-ON

Owner of the session

Runs the WhatsApp daemon (whatsapp-web.js, multi-account), captures the audio, and writes it to audio-inbox/. WhatsApp needs a persistent session to receive messages, so it lives wherever that daemon runs and survives reboots.

💻 CPU ONLY · ALWAYS-ON

The CPU that transcribes

A worker polls the queue every few seconds, pulls the file, ffmpeg ogg→wav 16 kHz, then faster-whisper large-v3-turbo in int8 on CPU. No accelerator is scheduled, woken or reserved: the node is always on and costs a few watts.

The path of one voice note

Where the 34 seconds actually went.

The pipeline never changed shape. What changed is the engine in the third box - and finding out that its cost had nothing to do with how long you spoke.

Voice notedaemon holds the session Queuepolled every few seconds Transcription engineVAD + int8 on CPU (VNNI) Text backas a message other services share the same engine
Features

What it does well.

🔐

100% local STT

faster-whisper large-v3-turbo int8 on CPU — no audio ever leaves your network. Multi-account: captures all your WhatsApp accounts at once.

↩️

Self-forward

Send yourself a voice note (an idea on the fly) and get the transcription back in your self-chat. Ideal for memos while driving, or for feeding long audio to an LLM that doesn't accept voice.

🏷️

Labeled output

The transcription includes account, time, sender name, and group. Transport over base64 over SSH — accents and line breaks survive correctly.

🧹

Clean dequeue

Files are deleted from the capture node only after successful delivery to the self-chat. Nothing gets lost or processed twice.

Engineering note

The @lid trap.

detecting a self-forwarded message
// WRONG — these will never match
if (msg.from === msg.to) { ... }
// WhatsApp serializes from as <number>@c.us
// and to as <id>@lid — never equal

// RIGHT
const dest = await client.getContactById(msg.to);
if (dest && dest.isMe) { enqueue(msg, "self"); }

Detecting a self-forward by comparing from === to doesn't work in WhatsApp Web: from is <number>@c.us and to is <id>@lid — two strings that are never equal even for the same person. The correct way is getContactById(to).isMe.

Depending on the library has a real cost: when WhatsApp renamed an internal field of the message id, audio downloads stopped for a full day — with text still flowing and every monitor green. The check that now covers it walks the same path production walks rather than an adjacent one, because the first version never serialised an id and would have stayed green throughout the outage.

whatsapp-web.js 1.34+faster-whisper int8 CPUlarge-v3-turboNode 20+Task SchedulerTailscale