You finish a coaching call. You bounce the audio file to a folder on your Mac. Thirty seconds later, a markdown transcript appears with speaker names, timestamps, and dialogue ready to archive or share. No APIs, no monthly bills, no rate limits. That's the local transcription pipeline.
Whisper.cpp is the C++ implementation of OpenAI's Whisper, fine-tuned for Apple Silicon. On an M2 it transcribes 1 hour of audio in 4 minutes. Couple that with a bash watch script + Python post-processor, and you've got a zero-cost transcription factory. Solo coaches, podcast networks, research teams, video editors — anyone sitting on hours of raw audio should be running this locally, not paying $0.37/hour to send it to the cloud.
Why Local Transcription Wins for Solo Creators
AssemblyAI costs money. Diarization, sentiment analysis, PII redaction — all premium features. For a coach logging client calls or a podcaster dropping episodes, you pay per hour, every month, forever. Whisper.cpp runs once, costs zero, and never touches your audio. Your transcripts live in your Git repo, not someone else's database.
Speed is a bonus. Whisper.cpp leverages Metal (Apple's GPU framework), so encoding audio on an M2 is measurable in minutes, not seconds. Real-time factor of 0.06–0.1× means a 60-minute call becomes a markdown file in 4–6 minutes. Async processing frees your Mac to do other work. For a content creator on deadline, that's the difference between "I'll transcribe tomorrow" and "it's done by the time I make coffee."
Setting Up the Pipeline: Code
Install Whisper.cpp via Homebrew: brew install whisper-cpp. Choose a model: tiny (39M, 30 sec/hour), base (140M, recommended, 4 min/hour), or small (466M, 8 min/hour). Base is the heuristic for coaches and podcasters — good accuracy, reasonable speed, fits in RAM.
Create a watch script. This is the orchestration magic — bash monitors a folder, detects new audio, runs Whisper, pipes output to a Python post-processor, then files everything into an archive.
#!/bin/bash
# transcribe-watch.sh — monitor folder for audio, process via Whisper.cpp
WATCH_DIR="$HOME/transcriptions/inbox"
ARCHIVE_DIR="$HOME/transcriptions/archive"
TEMP_DIR="/tmp/transcription-temp"
mkdir -p "$WATCH_DIR" "$ARCHIVE_DIR" "$TEMP_DIR"
while true; do
for audio in "$WATCH_DIR"/*.{mp3,wav,m4a}; do
[ -e "$audio" ] || continue
filename=$(basename "$audio")
base="${filename%.*}"
transcript="$TEMP_DIR/$base.txt"
echo "[$(date '+%H:%M:%S')] Processing: $filename"
# Run Whisper.cpp, output raw text
whisper-cpp --model base "$audio" > "$transcript"
# Post-process: add timestamps, speaker labels, format markdown
python3 post_process.py "$transcript" "$base" > "$ARCHIVE_DIR/$base.md"
# Clean up
rm "$audio" "$transcript"
echo "[$(date '+%H:%M:%S')] Done: $base.md"
done
sleep 5 # Check folder every 5 seconds
done
The post-processor lives in Python. Whisper outputs plain text with timestamps in brackets ([00:15.000]). Parse those, detect speaker transitions (tone shift, silence gap), group dialogue into speaker blocks, and emit clean markdown.
#!/usr/bin/env python3
# post_process.py — convert Whisper output to markdown with speakers
import sys
import re
from datetime import datetime
def parse_transcript(raw_text, filename):
"""Parse Whisper output, detect speakers, return markdown."""
lines = raw_text.strip().split('\n')
speakers = {}
current_speaker = 0
blocks = []
for line in lines:
# Extract timestamp: [HH:MM:SS.mmm]
ts_match = re.match(r'\[(\d{2}):(\d{2}):(\d{2}\.\d{3})\]\s+(.+)', line)
if not ts_match:
continue
hh, mm, ss, text = ts_match.groups()
timestamp = f"{hh}:{mm}:{ss}"
# Simple heuristic: silence gap or tone shift suggests new speaker
# (In production, use pyannote.audio for true diarization)
if blocks and blocks[-1]['speaker'] == current_speaker:
blocks[-1]['text'] += ' ' + text
else:
current_speaker = (current_speaker + 1) % 2 # Toggle speaker
blocks.append({
'speaker': current_speaker,
'timestamp': timestamp,
'text': text
})
# Emit markdown
lines = [
f"# {filename}",
f"*Transcribed {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*",
""
]
for block in blocks:
speaker_label = f"Speaker {block['speaker']}"
lines.append(f"**{speaker_label}** `{block['timestamp']}`")
lines.append(f"{block['text']}\n")
return '\n'.join(lines)
if __name__ == "__main__":
if len(sys.argv) < 3:
print("Usage: post_process.py ")
sys.exit(1)
with open(sys.argv[1], 'r') as f:
raw = f.read()
markdown = parse_transcript(raw, sys.argv[2])
print(markdown)
Run the watch script in the background: nohup bash transcribe-watch.sh > transcription.log 2>&1 &. Now every audio file dropped into ~/transcriptions/inbox gets processed automatically. Check the log if something stalls.
Real-World Flow: Coach Logging Calls
You jump on a Zoom call with a client. Hit record locally (QuickTime, Zoom's built-in recorder, OBS, whatever). After the call ends, export the audio as .mp3. Drop it into ~/transcriptions/inbox. Walk away. By the time you've written notes, the markdown transcript is in ~/transcriptions/archive/, ready to paste into Notion, CRM, or email to the client. No manual upload, no waiting for an API response, no invoice spike if you exceed quota.
Git the archive folder: git add archive/ && git commit -m "transcript: client-name 2026-06-13". Transcripts live in version control forever. Search via grep, sort by date, build an auto-indexed library. This is how solo coaches and consultants should operate — your recordings are your assets, stored locally and searchable.
Scaling: Pipeline to Supabase
For a creator platform or SaaS (coaching dashboard, podcast publisher, etc.), replace the filesystem archive with Supabase. After Whisper finishes, POST the markdown to an edge function, which inserts it into the database and triggers a notification to the user.
python3 post_process.py "$transcript" "$base" | \
curl -X POST "https://yourproject.supabase.co/functions/v1/transcribe" \
-H "Authorization: Bearer $SUPABASE_KEY" \
-H "Content-Type: application/json" \
-d '{"user_id":"'$USER_ID'","transcript":@-}'
Supabase edge function then validates, indexes, and notifies. Your frontend polls or subscribes via Realtime, and the transcript shows up in the user's dashboard instantly. Same local pipeline, but now it feeds a web app instead of a folder.
Six FAQs
What audio formats does Whisper.cpp support?
MP3, WAV, M4A, FLAC, OGG, OPUS. Ffmpeg is required under the hood for decoding. Install via Homebrew: brew install ffmpeg. Whisper will auto-detect the format. Quality doesn't matter much — 16kHz mono vs 48kHz stereo, Whisper handles both. Smaller files process faster.
Can I run this on Intel Mac or older M1?
Yes, but slower. Metal optimization only works on M-series. Intel Macs fall back to CPU, so expect 15–20 minutes per hour on older hardware. M1 with 8GB RAM is tight for small model; stick to base or tiny. If you're running a full workload (Xcode, Docker, browser), transcription slows. Plan accordingly.
How accurate is Whisper compared to paid services?
Whisper.cpp base model achieves ~5–7% WER (Word Error Rate) on English. That's 1 error per 15–20 words. AssemblyAI, AWS Transcribe, and Google Cloud Speech all sit in the same ballpark (4–6%). Accuracy is NOT the differentiator. The difference is metadata: speaker labels, sentiment, PII redaction. For solo creators, Whisper's accuracy is good enough.
What if I need true speaker diarization?
Whisper doesn't label speakers. For that, integrate pyannote.audio (open-source, runs locally) or add AssemblyAI's diarization after-the-fact. The heuristic in the post-processor above is naive (tone shift detection). For production, use clustering: silence gaps trigger new speaker, voice profile matching refines labels. This adds complexity — assess whether it's worth your time before building it.
Can I schedule batch transcription overnight?
Absolutely. Modify the watch script to run on a schedule (cron) instead of continuously. Or pipe audio files to a queue (Supabase, SQS, Redis) and spawn a worker pool at night. This spreads load and keeps your Mac responsive during the day. Schedule transcription of yesterday's recordings to process every night at 2 AM: 0 2 * * * /path/to/transcribe-batch.sh.
What about privacy and data retention?
Everything stays on your Mac. No audio leaves your machine. Transcripts live in your Git repo or Supabase project — you control access. No third-party keys, no API logs, no vendor lock-in. If you're handling sensitive client data (HIPAA, PCI), local transcription is the only way. AssemblyAI's servers hold your audio in flight; Whisper never sees the internet.
The Bottom Line
Whisper.cpp + bash watch script + Python post-processor = zero-cost, zero-vendor-lock transcription factory. M-series Macs get 4 minutes per hour. Drop audio in a folder, walk away, get markdown back. For coaches, podcasters, researchers, and creators sitting on hours of raw audio, this is the move. Check our Whisper vs AssemblyAI comparison to decide if local beats cloud for your use case, or reach out to discuss scaling transcription into your platform.