Engineering Research Brief / On-Device Speech / through early 2026

Building a live meeting mode that runs entirely on the CPU

State of the art in online/streaming speaker diarization and offline meeting transcription, evaluated against one hard constraint: fully offline, CPU-first, privacy-preserving, shippable to end users on Linux, macOS and Windows.

Context: faster-whisper STT · sherpa-onnx present Existing: offline file diarizer Need: continuous 1–2 h, unknown speaker count
00

Executive summary & recommendation

On-device transcription and on-device meeting notes are both solved problems in 2026. On-device diarization is not — and live/streaming diarization with an unknown speaker count is still a research frontier no open-source tool ships cleanly. The winning move is therefore not to chase real-time diarization but to reuse the offline diarizer you already have, and run it as a post-pass.

Recommendation

Ship a hybrid: stream the transcript live, diarize as a post-pass with sherpa-onnx.

Transcribe continuously with faster-whisper gated by Silero VAD, showing text as the meeting runs. Optionally show provisional speaker chips from cheap live embeddings. When the user presses stop, run your existing sherpa-onnx offline diarizer over the whole recording (segmentation + embedding + clustering with num_speakers = -1) to get accurate global labels and the true speaker count, reconcile against the live labels, then generate Markdown minutes with a small local LLM via llama.cpp.

This is exactly the production pattern of the local tool OpenWhispr: Silero VAD (2 MB) + pyannote-segmentation-3.0 (6.6 MB) + CAM++ (28 MB) through a native sherpa-onnx binary, CPU-only via ONNX Runtime — batch diarization of a 45-minute meeting in ~30 s on an M1, embeddings ~2 KB/speaker. It fits your stack with essentially no new heavy dependency and no gated model.

Why not the alternatives

Rejected · streaming EEND

NVIDIA Streaming Sortformer is real (arXiv 2507.18446) and permissively CC-BY-4.0, but it is GPU-first (all RTF numbers on an RTX 6000 Ada) and hard-capped at 4 speakers. Fails "CPU-first" and "unknown count".

Rejected · streaming clustering

diart is the one mature open streaming diarizer (MIT, runs on CPU, unknown count), but it depends on gated pyannote HF models — a real offline-installer friction, and it re-clusters, causing label churn.

Rejected · licensing

DiariZen is the SoTA open pipeline (DER ~13%) but its weights are CC-BY-NC (non-commercial) and WavLM-Large-based (heavy). NeMo Sortformer v1 is likewise CC-BY-NC. Both are un-shippable in a product.

The three things to accept

A

How you actually tell speakers apart

Neural speaker embeddings plus clustering — not "voice frequency" or pitch.

Why pitch / frequency alone fails

A single acoustic scalar like fundamental frequency (F0) cannot separate speakers, for two independent reasons that are well established in forensic phonetics:

A learned DNN embedding is trained end-to-end to be speaker-discriminative, capturing voice quality, spectral envelope and articulation jointly — placing each speaker at a point on an identity manifold where cosine distance separates them robustly across content and channel. This is the i-vector → x-vector generational shift. The canonical models: the x-vector (Snyder 2018), ECAPA-TDNN (Desplanques 2020, 0.87% EER on VoxCeleb1), and the GE2E d-vector (Wan 2018) behind Resemblyzer.

On-device embedding models — and what sherpa-onnx already ships

The pragmatic choice is a small Apache-2.0 model that sherpa-onnx already pre-converts to ONNX, so there is no custom export and no gated download. 3D-Speaker CAM++ (7.2M params, 0.65% EER) and ERes2Net-base (6.6M) are the best size/accuracy trade-off; sherpa-onnx also ships WeSpeaker and NeMo TitaNet variants.

ModelParamsVoxCeleb1 EERONNX / sherpa-onnxLicense
3D-Speaker CAM++7.2M0.65%shippedApache-2.0
3D-Speaker ERes2Net-base6.6M0.84%shippedApache-2.0
WeSpeaker ResNet34~6.6M0.72% (Vox1-H)shippedApache-2.0 (code)
NeMo TitaNet-large25.3M0.66%shippedCC-BY-4.0
ECAPA-TDNN (SpeechBrain)~22M0.80%via WeSpeaker onlyApache-2.0
Resemblyzer (GE2E)~1.4Mnot benchmarkedno ONNXApache-2.0

Takeaway. Your codebase already notes ECAPA is unreliable on sub-second windows. CAM++/ERes2Net are the right on-device choice: Apache-2.0, tiny, high accuracy, already ONNX-converted by sherpa-onnx, no HuggingFace token. Reuse the one you have for the file path.

B

Online vs offline diarization

Offline clustering is accurate and finds the speaker count globally, but produces no live labels. Online systems produce live labels but are either count-capped, GPU-bound, or gated.

Offline (the accurate baseline)

Segment → extract embeddings → cluster the whole recording, so the speaker count is discovered globally. The open standard is pyannote.audio 3.1 (powerset local segmentation + AHC over ECAPA embeddings); VBx (BUT) is the classic VB-HMM baseline. Your sherpa-onnx path is exactly this, offline-only, and it is the accuracy ceiling for a CPU app.

Online / streaming — the landscape

Two families discover new speakers on the fly. Clustering-based online methods (diart, UIS-RNN, online GMM/i-vector) spawn a new cluster when nothing matches → truly unbounded count. End-to-end streaming methods (Sortformer, FS-EEND) are faster and overlap-aware but capped at the training speaker count (≤4) — the open research gap.

SystemTypeUnknown count?On-device / CPULatencyShip license
sherpa-onnxOfflineyes · thresholdCPU / embeddedbatchApache-2.0
pyannote 3.1 / 4.xOfflineyes · autoCPU okbatchMIT code · gated wts
DiariZenOfflineyesWavLM-L, heavybatchCC-BY-NC weights
diart (Coria 2021)Onlineyes · incrementalCPU, ~12ms/stage0.5–5 sMIT · gated deps
UIS-RNNOnlineyes · ddCRPfeasiblelowunclear
Streaming SortformerOnlineno · max 4GPU-first0.32–30 sCC-BY-4.0
FS-EEND / BW-EDA-EENDOnlineno · ≤ train countresearch~1 s+research

The practical hybrid (recommended)

Stream transcription live; run the heavy diarization as a post-pass at meeting end (or in rolling chunks). The OpenWhispr production pattern makes this concrete: live path emits a provisional CAM++ label every ~1 s once ≥1.6 s of speech has accumulated; the end-of-meeting pass runs full offline diarization on the complete WAV for clean global labels, then reconciles. Their churn-control rules are worth copying verbatim:

Memory/compute for 1–2 h is a non-issue: 512-d embeddings ≈ 2 KB/speaker (~20 KB for a 2-h, 10-speaker meeting); the whole ONNX stack is ~45 MB and CPU-only. The trade-off is simply that final labels arrive at stop, not live — acceptable for a notes tool.

C

Estimating the number of speakers

Clustering picks k automatically — but expect it to be wrong in a meaningful minority of meetings, and to lean toward under-counting.

How k is chosen

Real-world accuracy

Design implication. Auto-estimate the count, but treat it as a first guess: surface it, let the user merge/split speakers, and lock corrections. Do not present the machine count as ground truth.

D

Overlapping speech

Clustering structurally cannot represent two people talking at once — and real meetings overlap 12–20% of the time.

Pragmatic stance for notes: perfect overlap attribution is not required for readable minutes. The powerset segmentation you already ship is enough; accept a small missed-speech floor rather than chasing overlap-aware resegmentation.

E

The toolkits, ranked for offline shipping

Only three options are clean to ship: sherpa-onnx, 3D-Speaker / WeSpeaker, and SpeechBrain — all Apache-2.0, ungated, self-contained, CPU-capable. Everything with a streaming or SoTA-accuracy story drags in a gated or non-commercial or GPU-only constraint.

ToolkitLicense to shipGated / HF tokenSelf-contained offline CPUUnknown countStreamingVerdict
sherpa-onnx Apache-2.0noyes · ONNX int8 CPU-firstyesbatch best fit
3D-Speaker / WeSpeaker Apache-2.0noyes · ONNX yesyesbatch clean
SpeechBrain Apache-2.0noyes PyTorch, heavieryesbatch recipe, not turnkey
diart MITyes · pyannote depsonly if bundled yes, real-timeyes yes only true streamer, but gated
pyannote.audio 3.x/4.x MIT code · CC-BY-4.0 wtsyes · token + termsonly if bundled yesyesbatch gated blocker
WhisperX BSD-2yes · pyannoteonly if bundled int8, slowyesbatch gated blocker
NeMo Streaming Sortformer CC-BY-4.0noyes GPU requiredno · max 4 yes GPU + 4-spk cap
NeMo Sortformer v1 CC-BY-NCnoyes GPU-onlyno · max 4batch non-commercial
whisper.cpp tinydiarize MITnoyes · GGML CPU, fastn/aper-file not real diarization

Hard blocker · license

NeMo Sortformer v1 and DiariZen weights are CC-BY-NC — cannot ship in a product.

Soft blocker · gated models

pyannote, diart, WhisperX need an HF token + accepting terms. Workaround: bundle pre-downloaded weights — but verify redistribution rights for gated repos first.

Not a diarizer

whisper.cpp tinydiarize marks speaker turns (small.en only), not who — no clustering, no identity. The -di flag is just L/R stereo channels.

Verdict. Stay on sherpa-onnx — you already use it, it's the most turnkey Apache-2.0 CPU diarizer, ungated, and handles unknown count. If you ever truly need live diarization, diart is the only mature open streamer, but budget for the gated-model bundling problem.

F

Aligning labels to words · endpointing

Word timestamps → speaker turns

Endpointing for continuous 1–2 h audio

For meeting mode: Silero VAD to chunk the stream + faster-whisper with word_timestamps=True for provisional live text, then re-align words to the offline diarization by max-overlap in the post-pass. If you need karaoke-tight timing in the final export, add wav2vec2 forced alignment (per-language model) — otherwise skip it.

G

Meeting notes from a local LLM

Fully offline minutes are realistic on CPU — provided you chunk by speaker turn and map-reduce, use separate schema-constrained passes, and budget minutes, not seconds.

The pipeline that works

What CPU can realistically run

From the one rigorous CPU-only benchmark (CEUR-WS 2025, llama.cpp Q4_K), generation throughput:

Model (Q4)Modern XeonOld Xeon (no AVX2)i7 laptopGood for
Llama-3.2-1B~120 tok/s~25 tok/shighlight summary
Phi-4-mini (3.8B)~15 tok/s~40 tok/ssummary / QA
Qwen2.5-7B~45 tok/s~8 tok/shigher quality
Mistral-Small 24B~15 tok/s~2 tok/stoo slow on CPU

The realistic band is Phi-4-mini, Qwen2.5-3B/7B, Llama-3.2-3B, Gemma-2-2B at Q4_K_M (<1% quality loss, ~75% smaller). CPU prefill of tens of thousands of tokens — not just generation — is the real cost, so end-to-end minutes for a 2 h meeting take minutes, not seconds. Grounding: AMI/ICSI/QMSum/MeetingBank are the corpora; AutoMin/ELITR is the minuting task; ROUGE is standard but a weak proxy, so evaluate qualitatively.

Prior OSS to learn from: ownscribe is closest to your design — local-first CLI, bundled Phi-4-mini GGUF, Markdown + JSON minutes (Summary / Key Points / Action Items / Decisions). Meetily uses Ollama. None ship GBNF-grammar JSON — a differentiator you can claim.

H

Prior art & the on-device bar

A fully-offline tool can already match cloud on transcription and on local-LLM notes. Cloud still leads on exactly two things — high-quality live diarization, and persistent voiceprint naming — and the second is an unoccupied on-device niche.

CapabilityFully on-device today?Who leads
Live long-form transcriptionyesApple SpeechAnalyzer · Whisper/Parakeet OSS
On-device notes / summariesyes · quality < cloudMeetily (Ollama) · Hyprnote (Qwen3-1.7B)
Post-hoc diarizationyes · beta-qualitypyannote (MacWhisper / WhisperX / Vibe)
Live / streaming diarizationemerging, not commoditypyannoteAI Live-1 (cloud)
Persistent voiceprint namingno on-device optionOtter (cloud, stored biometric)
Bot-free system-audio captureyesGranola · Hyprnote · Meetily · MacWhisper

Positioning. Your encrypted-corpus voiceprint (ADR-012) plus an offline diarization post-pass hits the two gaps at once: on-device diarization and persistent, private, cross-meeting speaker naming — which no on-device competitor offers.

Recommended architecture · live meeting mode

Capture → stream STT → live provisional labels → stop → offline diarization post-pass → reconcile → notes. Every stage CPU-only and offline; no gated model.

1
Capturemic + system audio → WAV ring buffer

Continuous record to disk (16 kHz mono). Keep the full WAV for the post-pass; ~1 GB/hr uncompressed is fine, or FLAC.

2
Stream STTSilero VAD → faster-whisper (word_timestamps)

VAD merges speech into ≤30 s windows; decode and show text live. This is your existing STT path, unchanged.

3
Live provisional speaker chipsoptional · CAM++ every ~1 s

Cheap embedding vs active + stored voiceprints → provisional "Speaker N". Cosmetic only; expect churn. Skip in v1 if you want.

↓  on stop  ↓
4
Offline diarization post-passsherpa-onnx · num_speakers = -1

pyannote-segmentation-3.0 (powerset, overlap-aware) + CAM++ embeddings + FastClustering over the whole WAV → accurate global labels + true count. ~30 s per 45 min on a laptop.

5
Align + reconcile + namemax-overlap · locked names · voiceprint match

Assign each Whisper word to its max-overlap speaker turn. Reconcile with live labels; never overwrite user-set names. Match clusters to enrolled voiceprints (ADR-012) for persistent naming.

6
Notesllama.cpp · Q4 · map-reduce · GBNF JSON

Chunk by turn, map-reduce, separate schema-constrained passes for summary / decisions / action-items → Markdown minutes + JSON. Budget minutes.

New dependencies: essentially none heavy — you already have sherpa-onnx (diarization), faster-whisper (STT + Silero VAD), and an optional llama.cpp for notes. The work is orchestration and the reconcile/naming UX, not new ML infrastructure.

!

Key risks

Gated & non-commercial modelsHigh · licensing

The best-accuracy and only-streaming options are all encumbered: pyannote/diart/WhisperX need an HF token + terms acceptance (offline-installer friction); DiariZen and NeMo Sortformer-v1 weights are CC-BY-NC (un-shippable). Staying on Apache-2.0 sherpa-onnx sidesteps all of it.

Unknown-count accuracyMedium · quality

Auto count estimation is wrong in ~1 session in 4 and under-counts. Mitigate with a user-facing merge/split UI and locked corrections — never present the machine count as final.

Overlapping speechMedium · quality

Meetings overlap 12–20%; clustering can't attribute overlaps, so some missed speech is unavoidable. The powerset segmentation you ship helps; don't chase perfect overlap attribution for notes.

CPU latency for notesMedium · perf

A 2 h transcript is tens of thousands of tokens; CPU prefill + map-reduce means minutes of wall-clock on a laptop with a 3–7B Q4 model. Run notes as a background job with progress, not a blocking call.

Live-label churnLow · UX

Provisional streaming labels re-cluster and swap. Treat them as cosmetic; the post-pass is the source of truth. Or omit live chips in v1.

Privacy / biometricLow · governance

Voiceprints are biometric. Your existing encrypted-corpus, opt-in, machine-bound design (ADR-011/012) already handles this correctly — keep naming opt-in and consent-gated, never auto-enroll.

§

Sources

Primary sources — arXiv papers, official repos, model cards and vendor docs. Verified directly where load-bearing (licenses, gating, speaker-count API).

A · Embeddings & models

  1. x-vector (Snyder 2018) — dl.acm.org/doi/10.1109/ICASSP.2018.8461375
  2. ECAPA-TDNN (Desplanques 2020) — arxiv.org/abs/2005.07143
  3. GE2E d-vector (Wan 2018) — arxiv.org/abs/1710.10467
  4. F0 in real-life speaker ID — researchgate.net/publication/276914683
  5. F0 & intelligibility (PMC) — pmc.ncbi.nlm.nih.gov/articles/PMC2885573
  6. i-vector→x-vector (Oxford Wave / IAFPA19) — oxfordwaveresearch.com …IAFPA19_xvectors.pdf
  7. 3D-Speaker / CAM++ — github.com/modelscope/3D-Speaker · arxiv.org/pdf/2303.00332
  8. WeSpeaker — github.com/wenet-e2e/wespeaker …pretrained.md
  9. NeMo TitaNet — huggingface.co/nvidia/speakerverification_en_titanet_large
  10. SpeechBrain ECAPA — huggingface.co/speechbrain/spkrec-ecapa-voxceleb
  11. sherpa-onnx model releases — github.com/k2-fsa/sherpa-onnx/releases …speaker-recognition-models

B · Online vs offline

  1. pyannote 3.1 (Bredin, Interspeech 2023) — isca-archive.org …bredin23_interspeech.pdf
  2. Review of online diarization (2024) — arxiv.org/html/2406.14464v1
  3. UIS-RNN — arxiv.org/abs/1810.04719
  4. EEND-EDA — arxiv.org/abs/2005.09921
  5. BW-EDA-EEND — arxiv.org/abs/2011.02678
  6. Sortformer — arxiv.org/abs/2409.06656
  7. Streaming Sortformer — arxiv.org/pdf/2507.18446 · HF model card
  8. DiariZen — github.com/BUTSpeechFIT/DiariZen
  9. diart (Coria 2021) — arxiv.org/pdf/2109.06483 · github.com/juanmc2005/diart
  10. sherpa-onnx diarization — k2-fsa.github.io/sherpa/onnx/speaker-diarization · API example
  11. OpenWhispr hybrid local diarization — openwhispr.com/blog/local-speaker-diarization
  12. SCDiar (2025) — arxiv.org/pdf/2501.16641

C / D · Count & overlap

  1. Wang 2018 (LSTM + eigengap) — wangquan.me …diarization_ICASSP_2018.pdf
  2. NME-SC (Park 2019) — arxiv.org/abs/2003.02405
  3. ECAPA embeddings for diarization (counting acc.) — arxiv.org/pdf/2104.01466
  4. Robustness of spectral clustering — arxiv.org/html/2403.14286v1
  5. Benchmarking diarization models (2025, overlap %) — arxiv.org/html/2509.26177v1
  6. Powerset multi-class CE — arxiv.org/html/2310.13025
  7. Bullock overlap-aware resegmentation — arxiv.org/pdf/1910.11646
  8. DIHARD-II is still hard — ar5iv.labs.arxiv.org/html/2002.12761

E / F · Toolkits, alignment & VAD

  1. pyannote 3.1 (gated) — huggingface.co/pyannote/speaker-diarization-3.1
  2. pyannote community-1 (CC-BY-4.0, gated) — huggingface.co/pyannote/speaker-diarization-community-1
  3. WhisperX — github.com/m-bain/whisperx
  4. NeMo Sortformer v1 (CC-BY-NC) — huggingface.co/nvidia/diar_sortformer_4spk-v1
  5. whisper.cpp tinydiarize — github.com/akashmjn/tinydiarize
  6. faster-whisper (VAD, word ts) — github.com/SYSTRAN/faster-whisper · issue #294
  7. CrisperWhisper — arxiv.org/pdf/2408.16589
  8. Word→speaker (pyannote+Whisper) — scalastic.io …whisper-pyannote
  9. Silero VAD — github.com/snakers4/silero-vad
  10. Pipecat smart-turn v3 — daily.co/blog …smart-turn-v3
  11. LiveKit turn detector — livekit.com/blog/solving-end-of-turn-detection

G · Meeting notes

  1. CPU llama.cpp benchmark (CEUR-WS 2025) — ceur-ws.org/Vol-4164/paper11.pdf
  2. MeetingBank (ACL 2023) — aclanthology.org/2023.acl-long.906.pdf
  3. QMSum — arxiv.org/abs/2104.05938
  4. AutoMin 2023 (INLG) — aclanthology.org/2023.inlg-genchal.19
  5. LLM×MapReduce (ACL 2025) — aclanthology.org/2025.acl-long.1341.pdf
  6. Gladia transcript→notes pipeline — gladia.io/blog/transcript-to-actionable-notes-llm
  7. ownscribe (local minutes) — github.com/paberr/ownscribe

H · Prior art

  1. Otter speaker ID — help.otter.ai …Speaker-Identification-Overview
  2. Fireflies trust FAQ — trust.fireflies.ai/faq
  3. Granola transcription — docs.granola.ai/article/transcription
  4. MacWhisper review — getvoibe.com/resources/macwhisper-review
  5. Meetily — github.com/Zackriya-Solutions/meetily
  6. Hyprnote (HN launch) — news.ycombinator.com/item?id=44725306
  7. Apple WWDC25 SpeechAnalyzer — developer.apple.com/videos/play/wwdc2025/277
  8. pyannoteAI streaming beta — pyannote.ai/changelog/streaming-diarization-beta