Local AI Meeting Transcription: Replace Otter.ai
Want to go deeper than this article?
Free account unlocks the first chapter of all 25 courses — RAG, agents, MCP, voice AI, MLOps, real GitHub repos.
Go from reading about AI to building with AI 20 structured courses. Hands-on projects. Runs on your machine. Start free.
Published on April 11, 2026 — 18 min read
Short answer: you can replace a transcription subscription with three local pieces — Whisper turns the audio into timestamped text, pyannote labels who said what, and a model served by Ollama turns the transcript into a summary with action items. It runs offline, has no recurring fee, and the complete Python script is below. What you give up is the real-time convenience of a hosted service and the electricity to run it.
This guide covers all four stages: capturing the audio, transcribing it, attributing it to speakers, and turning it into notes someone will actually read.
Why run meeting transcription locally? {#why-local}
Every hosted transcription service processes your audio on remote servers. That means your product roadmap discussions, hiring conversations, financial planning sessions and legal calls all pass through third-party infrastructure.
Three concrete reasons that matters:
Data residency. If you work with European clients, GDPR requires you to know where audio data is processed and stored, and for how long. That is a question you can answer definitively about a machine under your desk and only approximately about a vendor. Read the retention and "service improvement" clauses of whatever service you use — those clauses are where the surprises live.
Domain vocabulary. Whisper takes an initial_prompt that biases decoding toward terms you supply. Feed it your product names, your acronyms and your stack, and words like kubectl stop coming back as phonetic guesses. Hosted consumer services generally do not expose that control at all, which is a capability difference rather than a quality claim — you can steer the local model and you cannot steer theirs.
Cost shape. A subscription is per-seat and per-minute, so it scales with how much your team meets. Local transcription is a fixed hardware cost plus electricity, so it does not. Work out the crossover for your own team: multiply your seats by your provider's current published per-seat rate by twelve, and compare it against a GPU you may already own.
For a deeper look at the privacy implications, see our local AI privacy guide.
Reading articles is good. Building is better.
Free account = 20+ free chapters across 25 courses, with a per-chapter AI tutor. No card. Cancel anytime if you ever upgrade.
What does the pipeline look like? {#architecture}
The pipeline has four stages:
- Audio capture — record system audio or microphone input
- Transcription — Whisper converts speech to text with timestamps
- Speaker diarization — pyannote identifies who said what
- AI post-processing — Ollama generates summaries, decisions and action items
Hardware requirements:
| Component | Minimum | Recommended |
|---|---|---|
| RAM | 8 GB | 16 GB |
| GPU VRAM | 5 GB (medium model) | 10 GB (large-v3) |
| Storage | 5 GB | 20 GB |
| CPU | 4 cores | 8+ cores |
Those VRAM figures come from the model table in the OpenAI Whisper repository, not from anywhere else — see the section below.
Step 1: Install the transcription stack {#install}
Install Whisper
You have three options depending on your hardware. faster-whisper is the sensible default for most setups: it reimplements Whisper on CTranslate2, and its project README states it runs up to 4 times faster than openai/whisper at the same accuracy while using less memory. That is the maintainers' published figure, and their README shows the benchmark conditions behind it.
# Option A: faster-whisper (recommended — 4x speed, same accuracy)
pip install faster-whisper
# Option B: Original OpenAI Whisper
pip install openai-whisper
# Option C: whisper.cpp (best for CPU-only or Apple Silicon)
git clone https://github.com/ggerganov/whisper.cpp
cd whisper.cpp
make -j
# Download the large-v3 model for whisper.cpp
bash ./models/download-ggml-model.sh large-v3
For a complete walkthrough of all Whisper variants and their tradeoffs, see our Whisper local speech-to-text guide.
Install Speaker Diarization
# pyannote.audio for speaker identification
pip install pyannote.audio
# You need a Hugging Face token (free) for pyannote models
# Get one at https://huggingface.co/settings/tokens
# Accept the model terms at https://huggingface.co/pyannote/speaker-diarization-3.1
Install Ollama for Summarization
# macOS / Linux
curl -fsSL https://ollama.com/install.sh | sh
# Pull the summarization model
ollama pull llama3.2
# For better meeting summaries with longer context
ollama pull qwen2.5:14b
Check our Ollama Python API guide if you want to understand the API calls used throughout this script.
Install Supporting Libraries
pip install sounddevice soundfile numpy requests pydub
Step 2: Audio Capture {#audio-capture}
You need to get audio into a file. There are three paths depending on your meeting setup.
Option A: Record System Audio (Virtual Meetings)
For Zoom, Google Meet, or Teams calls, you need to capture system audio output.
macOS — BlackHole:
# Install BlackHole (virtual audio driver)
brew install --cask blackhole-2ch
# Create a Multi-Output Device in Audio MIDI Setup:
# 1. Open "Audio MIDI Setup" (Spotlight search)
# 2. Click "+" → Create Multi-Output Device
# 3. Check both "BlackHole 2ch" and your speakers/headphones
# 4. Set this Multi-Output Device as your system output
Linux — PulseAudio:
# Create a virtual sink to capture system audio
pactl load-module module-null-sink sink_name=meeting_capture sink_properties=device.description="Meeting_Capture"
# Route system audio to both speakers and capture sink
pactl load-module module-loopback source=meeting_capture.monitor
# Record from the virtual sink
ffmpeg -f pulse -i meeting_capture.monitor -ac 1 -ar 16000 meeting.wav
Option B: Record Microphone Input
For in-person meetings where you want to capture room audio:
import sounddevice as sd
import soundfile as sf
import numpy as np
SAMPLE_RATE = 16000 # Whisper expects 16kHz
CHANNELS = 1
print("Recording... Press Ctrl+C to stop.")
frames = []
try:
with sd.InputStream(samplerate=SAMPLE_RATE, channels=CHANNELS) as stream:
while True:
data, _ = stream.read(SAMPLE_RATE) # 1-second chunks
frames.append(data.copy())
except KeyboardInterrupt:
print("Recording stopped.")
audio = np.concatenate(frames, axis=0)
sf.write("meeting.wav", audio, SAMPLE_RATE)
print(f"Saved {len(audio) / SAMPLE_RATE:.1f} seconds to meeting.wav")
Option C: Upload Existing Recording
Most meeting platforms let you download recordings. Whisper handles mp3, mp4, wav, m4a, and webm natively. For other formats:
# Convert any audio/video to Whisper-compatible format
ffmpeg -i meeting_recording.mp4 -ac 1 -ar 16000 -acodec pcm_s16le meeting.wav
Reading articles is good. Building is better.
Free account = 20+ free chapters across 25 courses, with a per-chapter AI tutor. No card. Cancel anytime if you ever upgrade.
Step 3: which Whisper model should you run? {#model-selection}
Model choice is the biggest decision you make, and it is one where published numbers already exist — so use those rather than a figure invented for a blog post.
The table below is OpenAI's own published model table: parameter count, the VRAM the reference implementation requires, and speed relative to large (large = 1x). Relative speed is the useful column, because it survives a hardware change in a way that "minutes per hour of audio" never does.
| Model | Parameters | Required VRAM | Relative speed (OpenAI's figures) |
|---|---|---|---|
| tiny | 39 M | ~1 GB | ~10x |
| base | 74 M | ~1 GB | ~7x |
| small | 244 M | ~2 GB | ~4x |
| medium | 769 M | ~5 GB | ~2x |
| large (v2/v3) | 1550 M | ~10 GB | 1x |
| turbo | 809 M | ~6 GB | ~8x |
Two models sit outside that table and are worth knowing about:
- distil-large-v3 — a distilled variant published by Hugging Face. Its model card documents the size, the speed-up over large-v3, and the WER it was evaluated at on named datasets. Read those numbers there; they are the ones the authors stand behind.
- faster-whisper — not a model but a runtime. It runs any of the above on CTranslate2, and int8 or float16 compute types cut memory further.
How to pick, without a benchmark you cannot verify: run medium if you have a 6-8 GB card, large-v3 if you have 10 GB or more and the recording matters (legal, compliance, interviews), and a distilled variant when you want most of large's quality at a fraction of the compute. Then check the result against your own audio, because the only word error rate that matters to you is the one on the way your meetings sound.
Where do the real accuracy numbers live?
If you want to compare ASR systems honestly, use a leaderboard that runs every model over the same corpora with the same scoring: the Open ASR Leaderboard reports WER and inference speed for open speech models across a standard set of datasets, and it is updated as models ship.
Two things it will not tell you, and that decide most real deployments:
- Vocabulary steering. Whisper's
initial_promptlets you bias decoding toward your own jargon. This is a capability a hosted consumer service typically does not expose, and it is the difference betweenkubectland a phonetic guess. - Latency shape. A hosted service transcribes while the meeting happens. This pipeline transcribes afterwards. That is a genuine convenience gap, not a quality one, and the real-time section below only partly closes it.
Step 4: the complete transcription script {#transcription-script}
This is the full pipeline. Save it as transcribe_meeting.py:
#!/usr/bin/env python3
"""
Local meeting transcription pipeline.
Whisper (transcription) + pyannote (diarization) + Ollama (summarization)
"""
import sys
import json
import requests
from pathlib import Path
from datetime import timedelta
# --- Configuration ---
WHISPER_MODEL = "large-v3" # Options: tiny, base, small, medium, large-v3
OLLAMA_MODEL = "llama3.2" # For summarization
OLLAMA_URL = "http://localhost:11434"
HF_TOKEN = "your_huggingface_token" # Required for pyannote
# Domain-specific vocabulary — add your company terms here
INITIAL_PROMPT = "Kubernetes, kubectl, PostgreSQL, Redis, GraphQL, microservices, CI/CD"
def transcribe_audio(audio_path: str) -> dict:
"""Transcribe audio file with timestamps using faster-whisper."""
from faster_whisper import WhisperModel
model = WhisperModel(WHISPER_MODEL, device="auto", compute_type="float16")
segments_raw, info = model.transcribe(
audio_path,
beam_size=5,
language="en",
initial_prompt=INITIAL_PROMPT,
vad_filter=True, # Skip silence automatically
vad_parameters=dict(
min_silence_duration_ms=500,
speech_pad_ms=200,
),
word_timestamps=True,
)
segments = []
full_text = []
for seg in segments_raw:
segments.append({
"start": seg.start,
"end": seg.end,
"text": seg.text.strip(),
})
full_text.append(seg.text.strip())
return {
"language": info.language,
"duration": info.duration,
"segments": segments,
"full_text": " ".join(full_text),
}
def diarize_speakers(audio_path: str, num_speakers: int = None) -> list:
"""Identify speakers in the audio using pyannote."""
from pyannote.audio import Pipeline
pipeline = Pipeline.from_pretrained(
"pyannote/speaker-diarization-3.1",
use_auth_token=HF_TOKEN,
)
diarization = pipeline(audio_path, num_speakers=num_speakers)
speaker_segments = []
for turn, _, speaker in diarization.itertracks(yield_label=True):
speaker_segments.append({
"start": turn.start,
"end": turn.end,
"speaker": speaker,
})
return speaker_segments
def merge_transcript_speakers(transcript: dict, speaker_segments: list) -> str:
"""Combine Whisper transcript with speaker labels."""
output_lines = []
current_speaker = None
for seg in transcript["segments"]:
seg_mid = (seg["start"] + seg["end"]) / 2
# Find which speaker is active at this segment's midpoint
speaker = "Unknown"
for sp in speaker_segments:
if sp["start"] <= seg_mid <= sp["end"]:
speaker = sp["speaker"]
break
timestamp = str(timedelta(seconds=int(seg["start"])))
if speaker != current_speaker:
current_speaker = speaker
output_lines.append(f"\n[{timestamp}] **{speaker}:**")
output_lines.append(seg["text"])
return "\n".join(output_lines)
def remove_filler_words(text: str) -> str:
"""Strip filler words while preserving sentence structure."""
fillers = [
" um ", " uh ", " like, ", " you know, ",
" basically, ", " actually, ", " sort of ",
" kind of ", " I mean, ",
]
cleaned = text
for filler in fillers:
cleaned = cleaned.replace(filler, " ")
# Collapse multiple spaces
while " " in cleaned:
cleaned = cleaned.replace(" ", " ")
return cleaned
def summarize_with_ollama(transcript: str, model: str = OLLAMA_MODEL) -> str:
"""Generate meeting summary, decisions, and action items."""
prompt = f"""You are a meeting analyst. Analyze this transcript and produce a structured summary.
TRANSCRIPT:
{transcript[:12000]}
Produce EXACTLY this format:
## Meeting Summary
(3-5 sentence overview of what was discussed)
## Key Decisions
- (List each decision made, with context)
## Action Items
- [ ] (Task) — Owner: (Person) — Due: (Date if mentioned, otherwise "TBD")
## Open Questions
- (Questions raised but not resolved)
## Follow-Up Topics
- (Items that need discussion in the next meeting)
Be specific. Use names and details from the transcript. Do not invent information."""
response = requests.post(
f"{OLLAMA_URL}/api/generate",
json={"model": model, "prompt": prompt, "stream": False},
timeout=120,
)
return response.json()["response"]
def process_meeting(audio_path: str, num_speakers: int = None) -> None:
"""Full pipeline: transcribe → diarize → summarize → output."""
audio_file = Path(audio_path)
if not audio_file.exists():
print(f"Error: {audio_path} not found")
sys.exit(1)
print(f"Processing: {audio_file.name}")
output_base = audio_file.stem
# Step 1: Transcribe
print("Step 1/4: Transcribing with Whisper...")
transcript = transcribe_audio(audio_path)
duration_min = transcript["duration"] / 60
print(f" Duration: {duration_min:.1f} minutes")
print(f" Segments: {len(transcript['segments'])}")
# Step 2: Speaker diarization
print("Step 2/4: Identifying speakers...")
speakers = diarize_speakers(audio_path, num_speakers)
unique_speakers = set(s["speaker"] for s in speakers)
print(f" Detected {len(unique_speakers)} speakers")
# Step 3: Merge and clean
print("Step 3/4: Merging transcript with speaker labels...")
merged = merge_transcript_speakers(transcript, speakers)
cleaned = remove_filler_words(merged)
# Step 4: AI Summary
print("Step 4/4: Generating AI summary...")
summary = summarize_with_ollama(cleaned)
# Write outputs
output_md = f"""# Meeting Transcript: {audio_file.stem}
**Date:** {audio_file.stat().st_mtime}
**Duration:** {duration_min:.1f} minutes
**Speakers:** {', '.join(sorted(unique_speakers))}
---
{summary}
---
## Full Transcript
{cleaned}
"""
output_path = f"{output_base}_notes.md"
Path(output_path).write_text(output_md)
print(f"\nDone! Meeting notes saved to: {output_path}")
# Also save raw JSON for programmatic access
json_path = f"{output_base}_raw.json"
Path(json_path).write_text(json.dumps({
"transcript": transcript,
"speakers": speakers,
"summary": summary,
}, indent=2))
print(f"Raw data saved to: {json_path}")
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python transcribe_meeting.py <audio_file> [num_speakers]")
print("Example: python transcribe_meeting.py meeting.wav 4")
sys.exit(1)
audio = sys.argv[1]
speakers = int(sys.argv[2]) if len(sys.argv) > 2 else None
process_meeting(audio, speakers)
Run it:
# Basic usage — auto-detect speakers
python transcribe_meeting.py meeting.wav
# Specify number of speakers for better accuracy
python transcribe_meeting.py standup.wav 5
# Process a Zoom recording directly
python transcribe_meeting.py ~/Downloads/zoom_recording.mp4 3
Step 5: Real-Time Transcription {#real-time}
The batch script above works great for recordings. For live meetings where you want captions as people talk, use distil-whisper with streaming:
#!/usr/bin/env python3
"""Real-time meeting transcription with live captions."""
import sounddevice as sd
import numpy as np
from faster_whisper import WhisperModel
import sys
model = WhisperModel("distil-large-v3", device="auto", compute_type="float16")
SAMPLE_RATE = 16000
CHUNK_DURATION = 5 # Transcribe every 5 seconds
print("Live transcription started. Speak into your microphone.")
print("Press Ctrl+C to stop.\n")
buffer = np.array([], dtype=np.float32)
def audio_callback(indata, frames, time_info, status):
global buffer
buffer = np.append(buffer, indata[:, 0])
if len(buffer) >= SAMPLE_RATE * CHUNK_DURATION:
segments, _ = model.transcribe(
buffer,
beam_size=1,
language="en",
vad_filter=True,
)
for seg in segments:
text = seg.text.strip()
if text:
print(f" {text}")
buffer = np.array([], dtype=np.float32)
try:
with sd.InputStream(
samplerate=SAMPLE_RATE,
channels=1,
callback=audio_callback,
blocksize=SAMPLE_RATE,
):
while True:
sd.sleep(100)
except KeyboardInterrupt:
print("\nTranscription stopped.")
Be honest with yourself about the latency here, because the arithmetic is unforgiving:
caption delay >= CHUNK_DURATION + transcription time for that chunk
With CHUNK_DURATION = 5 you cannot see a word sooner than five seconds after it is spoken, no matter how fast your GPU is — the buffer has to fill first. Shrinking the chunk cuts the delay but starves the model of context, and accuracy falls with it because Whisper is a sequence model that uses what came before. Two to five seconds is the usual compromise. If you need genuinely live captions, that is the one place a hosted service still has a structural advantage.
Step 6: batch processing {#batch-processing}
After a day of meetings, you probably have 4-6 recordings sitting in a folder. Process them all overnight:
#!/bin/bash
# batch_transcribe.sh — Process all audio files in a directory
INPUT_DIR="${1:-.}"
OUTPUT_DIR="${2:-./transcripts}"
mkdir -p "$OUTPUT_DIR"
count=0
for file in "$INPUT_DIR"/*.{wav,mp3,mp4,m4a,webm}; do
[ -f "$file" ] || continue
count=$((count + 1))
echo "[$count] Processing: $(basename "$file")"
python transcribe_meeting.py "$file"
mv "$(basename "$file" | sed 's/\.[^.]*$//')_notes.md" "$OUTPUT_DIR/"
mv "$(basename "$file" | sed 's/\.[^.]*$//')_raw.json" "$OUTPUT_DIR/" 2>/dev/null
done
echo "Done! Processed $count files. Results in $OUTPUT_DIR/"
# Process all recordings from today
./batch_transcribe.sh ~/recordings/2026-04-11 ~/meeting-notes/
# Process with nohup so it runs after you close your laptop
nohup ./batch_transcribe.sh ~/recordings/ ~/notes/ > transcribe.log 2>&1 &
Improving Accuracy {#improving-accuracy}
Custom Vocabulary Prompts
Whisper accepts an initial prompt that biases it toward specific terms. This is the single most impactful accuracy tweak for domain-specific meetings:
# Engineering standup
INITIAL_PROMPT = """Kubernetes, kubectl, PostgreSQL, Redis, GraphQL,
microservices, CI/CD, sprint, Jira, pull request, deployment pipeline,
staging environment, load balancer, Docker, Terraform"""
# Sales meeting
INITIAL_PROMPT = """ARR, MRR, churn rate, pipeline, qualified lead,
ACV, enterprise deal, proof of concept, stakeholder, procurement,
renewal, upsell, customer success"""
# Medical consultation (HIPAA-sensitive — exactly why you run locally)
INITIAL_PROMPT = """diagnosis, prognosis, contraindication, dosage,
milligrams, CBC, MRI, CT scan, referral, follow-up, prescription"""
Timestamp Alignment Tuning
If timestamps drift on long recordings, adjust the VAD parameters:
segments, info = model.transcribe(
audio_path,
vad_filter=True,
vad_parameters=dict(
threshold=0.35, # Lower = more sensitive to speech
min_silence_duration_ms=300, # Shorter silence gaps
speech_pad_ms=150, # Tighter segment boundaries
min_speech_duration_ms=250, # Ignore very short sounds
),
)
Noise Reduction Preprocessing
Office recordings with HVAC noise, keyboard clicks, or background chatter benefit from preprocessing:
# Remove background noise with ffmpeg (high-pass + low-pass filter)
ffmpeg -i noisy_meeting.wav -af "highpass=f=100, lowpass=f=8000, afftdn=nf=-20" clean_meeting.wav
# For aggressive noise reduction, use RNNoise
# Install: pip install rnnoise-python
python -c "
import rnnoise
denoiser = rnnoise.RNNoise()
# Process 10ms frames at 48kHz
"
Output Formats and Integration {#output-formats}
Email Meeting Notes to Attendees
Add this to the end of your pipeline to automatically email the summary:
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
def email_notes(summary: str, recipients: list, subject: str):
"""Send meeting notes via local SMTP or configured relay."""
msg = MIMEMultipart()
msg["From"] = "meeting-bot@yourcompany.com"
msg["To"] = ", ".join(recipients)
msg["Subject"] = f"Meeting Notes: {subject}"
msg.attach(MIMEText(summary, "markdown"))
with smtplib.SMTP("localhost", 587) as server:
server.send_message(msg)
print(f"Notes emailed to {len(recipients)} recipients")
# Usage after transcription
email_notes(
summary,
recipients=["team@company.com", "manager@company.com"],
subject="Sprint Planning - April 11"
)
Export to Notion, Obsidian, or Jira
The output is standard Markdown with action items formatted as checkboxes. It drops directly into:
- Obsidian — Move the .md file to your vault folder. If you have our Obsidian AI integration set up, it will be automatically indexed for semantic search.
- Notion — Paste the markdown content; Notion auto-formats headings, checkboxes, and bullet points.
- Jira — Parse the action items programmatically and create tickets via the Jira API.
Performance Tuning {#performance}
Model Loading Optimization
The first transcription is slow because Whisper loads the model into GPU memory. Keep the model loaded between transcriptions:
# Load model once, reuse across multiple files
model = WhisperModel("large-v3", device="cuda", compute_type="float16")
# Process multiple files without reloading
for audio_file in meeting_files:
segments, info = model.transcribe(audio_file, beam_size=5)
# ... process segments
GPU Memory Management
If you are running Ollama for summarization on the same GPU as Whisper, sequence the operations rather than running them simultaneously:
# Step 1: Transcribe (Whisper uses GPU)
transcript = transcribe_audio(audio_path)
# Step 2: Free GPU memory before Ollama needs it
import gc
import torch
del model
gc.collect()
if torch.cuda.is_available():
torch.cuda.empty_cache()
# Step 3: Summarize (Ollama uses GPU)
summary = summarize_with_ollama(transcript["full_text"])
CPU-Only Performance Tips
No GPU? Whisper still works, just slower. These settings optimize CPU inference:
model = WhisperModel(
"medium", # large-v3 is too slow on CPU
device="cpu",
compute_type="int8", # 2x faster than float32 on CPU
cpu_threads=8, # Match your core count
)
CPU transcription is slower than GPU by roughly the ratio of their memory bandwidths, which is typically an order of magnitude — but it is unattended work. Kick a day of recordings off before you leave and the wall-clock time stops mattering. Time your own machine once with time python transcribe_meeting.py sample.wav on a five-minute clip and multiply; that single measurement is worth more than any table of somebody else's hardware.
What limits accuracy in practice? {#benchmarks}
Word error rate on a benchmark corpus is not what will bite you. These four conditions are, and they degrade every ASR system, hosted or local:
| Condition | Why it hurts | What actually helps |
|---|---|---|
| Crosstalk — two people at once | The model decodes one stream; overlapping speech has no correct single transcript | Better mics, a meeting norm, or per-speaker channels if you can get them |
| Speakerphone / phone audio | Narrowband 8 kHz audio has lost the high frequencies that distinguish consonants | Upsample to 16 kHz before decoding; expect a real quality floor regardless |
| Domain jargon and product names | The token never appeared often enough in training | initial_prompt with your terms — the single biggest lever you control |
| Background noise (HVAC, keyboards) | Non-speech energy competes with the signal | The ffmpeg filter chain above; VAD to skip silence |
For system-versus-system word error rates, use the Open ASR Leaderboard, which scores models over the same public datasets rather than over anyone's private recordings.
Speaker diarization
pyannote publishes per-dataset diarization error rates on its speaker-diarization-3.1 model card, including the benchmark corpora used — check the figures there, and note how widely they vary between corpora. That variance is the point: a DER quoted without naming the corpus is meaningless.
Structurally, the failure modes you should expect are short interjections ("yeah", "right", "mmhmm") landing on the wrong speaker, and accuracy falling as the number of people sharing one microphone rises. Passing num_speakers when you know the headcount removes one source of error for free.
Frequently asked questions
Can this handle meetings in languages other than English?
Whisper is multilingual — OpenAI's model card lists the supported languages and the per-language WER it was evaluated at, and the spread between the best- and worst-served languages is large. Set language="es" (or any ISO code) in the transcribe call. For mixed-language meetings, omit the parameter and let Whisper detect per segment; detection itself becomes a source of error when speakers switch mid-sentence.
How good is pyannote at telling speakers apart?
Check the per-corpus diarization error rates on the pyannote model card — they vary enough between datasets that a single headline number would mislead you. What is reliably true: offline diarization can use the whole recording, which is an advantage over anything labelling speakers live, and passing num_speakers when you know the headcount improves attribution.
What happens if my GPU runs out of memory?
Drop to a smaller model, switch compute_type to int8, or run on CPU. The script's device="auto" falls back to CPU when there is not enough VRAM. Use the published VRAM column in the model table above to pick before you hit the error.
Can I transcribe phone calls?
Yes, with a caveat. Record with a call-recording app or capture system audio via PulseAudio/BlackHole. Telephony audio is narrowband — typically 8 kHz — which permanently discards the high frequencies that separate similar consonants. Upsampling to 16 kHz before transcription helps the model's expectations, but it cannot restore information that was never captured.
Is real-time transcription good enough for live captions?
It depends on what you mean by real-time. As the arithmetic in the streaming section shows, your floor is the chunk length — five seconds in the script above. Text arrives in bursts, not word by word, and each chunk is decoded with less surrounding context than a full-file pass would give it. Treat live output as a working aid and reprocess the recording afterwards for anything that goes on the record.
How much disk space do the models need?
Check each download size on its own model page — they change between releases. As a rough planning figure, budget in the region of 6 GB for a large Whisper checkpoint, the pyannote models and a small summarisation model together.
Audio is cheap by comparison, and you can compute it exactly rather than guessing:
bytes = sample rate x bytes per sample x channels x seconds
16000 x 2 x 1 x 3600 = 115,200,000 bytes ~= 115 MB per hour
That is uncompressed 16 kHz mono PCM, which is what Whisper wants. Encode the archive copy as FLAC or Opus if you are keeping months of it.
Can I use this for legal or compliance recordings?
This is one of the strongest cases for running it locally, because the audio never leaves a machine you control. Use the largest model your hardware supports, keep recordings and transcripts on encrypted storage with access controls, and document chain of custody. Recording consent law varies by jurisdiction and by who is on the call — that part is a question for your legal team, not for a setup guide.
Conclusion
A transcription subscription is a recurring cost that scales with how much your team meets, and it routes confidential conversations through infrastructure you have not audited. This pipeline replaces it with hardware you may already own: Whisper for the transcript, pyannote for the speaker labels, Ollama for the summary and action items.
Be clear about the trade. You lose real-time captions and a polished web app. You gain vocabulary steering, no per-seat fee, and audio that never leaves the building. Once the script is configured, processing a recording is one command, and batch mode clears a day of meetings while you sleep.
For more on building private AI workflows, explore our guide to running AI for small businesses, and the Whisper local speech-to-text guide for deeper coverage of the transcription layer on its own.
Go from reading about AI to building with AI
20 structured courses. Hands-on projects. Runs on your machine. Start free.
Liked this? 20 full AI courses are waiting.
From fundamentals to RAG, agents, MCP servers, voice AI, and production deployment with real GitHub repos. First chapter free, every course.
Build Real AI on Your Machine
RAG, agents, NLP, vision, and MLOps - chapters across 25 courses that take you from reading about AI to building AI.
Want structured AI education?
25 courses, 519+ chapters, from $9. Understand AI, don't just use it.
Continue Your Local AI Journey
Comments (0)
No comments yet. Be the first to share your thoughts!