hybrid-transcribe/
output/
demo.json
demo.txt
inqua-run.log
run.log
study.csv
study.md
study.pdf
study.typ
samples/
inqua/
demo-de.wav
study-A.wav
study-B.wav
scripts/
fetch-sample-audio.sh
generate-demo-audio.sh
prepare-study-clips.sh
Sources/
diarization-study/
hybrid-transcribe/
Package.resolved
Package.swift
PROJECT.yaml
README.md
run.sh
[DRAFT — edit this] > Best-in-class local speech processing on Apple Silicon — zero cloud, zero privacy compromise. Inferred stage: development. Define the specific deliverable, success state, and timeline.
Success criteria:| Code | /Users/friedrichstr/ai-mac-studio/development/hybrid-transcribe |
| Check | Path | Status | Detail |
|---|---|---|---|
| code | /Users/friedrichstr/ai-mac-studio/development/hybrid-transcr | ✓ ok | 9 items, last_modified=2026-08-24 |
Best-in-class local speech processing on Apple Silicon — zero cloud, zero privacy compromise.
This project benchmarks and implements the optimal pipeline for transcribing multi-speaker audio entirely on-device, combining automatic speech recognition (ASR) with speaker diarization via the speech-swift library.
Based on a full comparative study on real German multi-speaker recordings:
| Component | Engine | Why |
|---|---|---|
| ASR | Apple SpeechTranscriber (de-DE) |
Built-in, 75× RTFx, best German quality, zero download |
| Diarization | SortformerDiarizer (CoreML) |
Correct speaker count on all test files, 19–37× RTFx, fastest |
# Transcribe a file with the recommended pipeline
swift run hybrid-transcribe meeting.wav -o transcript.txt --json transcript.json
Output:
[0:00.3–0:12.1] Speaker 1: Ich würde mich sehr freuen, wenn wir die Kochplatte nach rechts versetzen.
[0:12.4–0:14.2] Speaker 2: Welche Hochplatte meinst du? An der Wand?
[0:14.5–0:55.0] Speaker 1: Das finde ich auch. Du hast ja weniger gekocht ...
Audio file (WAV/M4A/FLAC)
│
├──▶ SortformerDiarizer (CoreML, Neural Engine)
│ NVIDIA Sortformer — end-to-end diarization
│ → who spoke when (speaker segments)
│
└──▶ Apple SpeechTranscriber (de-DE, Speech API)
word-level timestamps
→ what was said
│
▼
Segment merger (time overlap)
│
▼
Speaker-attributed transcript
Both pipelines run on the same audio. Results are merged by time overlap: each word is assigned to the speaker segment that covers its midpoint.
Measured on two real German legal office recordings (Kanzlei planning sessions), 10-minute clips, against Gemini 3-flash-preview ground-truth transcripts.
| Engine | Backend | File A (2 spk) | File B (3 spk) | Avg RTFx | Segments/min |
|---|---|---|---|---|---|
sortformer |
CoreML ANE | 2 ✓ | 3 ✓ | 28× | 10 |
pyannote-mlx |
MLX GPU | 3 ✗ (+1) | 7 ✗ (+4) | 18× | 23 |
pyannote-coreml |
CoreML ANE | 3 ✗ (+1) | 7 ✗ (+4) | 16× | 23 |
Pyannote over-estimates speaker count on German conversational audio — its default clustering threshold is tuned on English benchmarks. Sortformer's end-to-end CoreML approach is both more accurate and 1.5–2× faster.
| File | Words | Vocab recall vs GT | RTFx | Wall time |
|---|---|---|---|---|
| File A (2 speakers) | 1,105 | 65.0% | 75× | 8 s |
| File B (3 speakers) | 1,270 | 38.4% | 77× | 8 s |
Vocabulary recall is measured against the full-recording Gemini GT (10-min clip ≈ 25% of total), so recall is underestimated. Qualitative review confirms transcription accuracy is high for German.
| Engine | File A switches/min | File B switches/min | Singleton segs (<1 s) |
|---|---|---|---|
sortformer |
3.2 | 8.4 | low |
pyannote-mlx |
18+ | 45+ | high (false splits) |
Three engines are available. All run fully on-device on Apple Silicon.
| Engine | Backend | Model | German | RTFx | Use case |
|---|---|---|---|---|---|
| Apple SpeechTranscriber | Speech API | built-in | ✓ native | ~75× | Production default |
| Qwen3-ASR 0.6B | MLX GPU | ~380 MB | ✓ (52 langs) | ~5× | High-accuracy alternative |
| Parakeet TDT | CoreML ANE | ~230 MB | ✓ (25 EU langs) | ~6× | Low-power alternative |
# Run with Qwen3-ASR instead of Apple (first run downloads ~380 MB)
swift run diarization-study --asr qwen3-asr --diarizers sortformer --file both
# Compare all three ASR engines
swift run diarization-study --asr all --diarizers sortformer --file both
ffmpeg — for audio conversion (brew install ffmpeg)~/Library/Caches/| Model | Size | Cached at |
|---|---|---|
| Sortformer (CoreML) | ~230 MB | ~/Library/Caches/ |
| Pyannote Segmentation (MLX) | ~12 MB | ~/Library/Caches/ |
| WeSpeaker ResNet34-LM (MLX) | ~18 MB | ~/Library/Caches/ |
| Qwen3-ASR 0.6B (MLX, optional) | ~380 MB | ~/Library/Caches/ |
| Parakeet TDT (CoreML, optional) | ~230 MB | ~/Library/Caches/ |
cd ~/ai-mac-air/development/hybrid-transcribe
# Transcribe a meeting recording
swift run hybrid-transcribe recording.m4a -o transcript.txt --json transcript.json -v
# Use Neural Engine instead of GPU for speaker embeddings (pyannote only)
swift run hybrid-transcribe recording.wav --embedding-engine coreml
# Run the full diarization + ASR comparative study
./scripts/prepare-study-clips.sh # prepares 10-min WAV clips
swift run diarization-study --file both # runs all engines, writes reports
hybrid-transcribehybrid-transcribe <audio> [options]
Options:
--locale de-DE ASR language (default: de-DE)
--embedding-engine mlx WeSpeaker on GPU (default)
--embedding-engine coreml WeSpeaker on Neural Engine
-o, --output PATH Write plain-text transcript
--json PATH Write JSON with speaker/start/end/text
-v, --verbose Show diarization progress
-h, --help
diarization-studydiarization-study [options]
Diarization engines: pyannote-mlx | pyannote-coreml | sortformer
ASR engines: apple-speech | qwen3-asr | parakeet
Options:
--file A|B|both Audio clips to process (default: both)
--diarizers all|a,b,... Diarizer IDs (default: all)
--asr all|a,b,... ASR engine IDs (default: all)
--root PATH Project root (default: auto-detected)
-v, --verbose Show per-engine progress
-h, --help
Output:
output/study.csv Raw metrics
output/study.md Markdown report
output/study.typ Typst source (compile to PDF with typst 0.15)
# Compile the PDF report
typst compile output/study.typ output/study.pdf
Ultra-Sortformer (mago-research) is a NeMo-based benchmark extending NVIDIA Sortformer to 8 speakers. Its published results strongly validate the Sortformer approach for German:
| Dataset | Ultra-Sortformer DER | Pyannote DER |
|---|---|---|
| CallHome German | 6.70% | 17.11% |
| CallHome English | 4.94% | 15.86% |
| AMI IHM | 26.05% | 12.13% |
| Total (real) | 16.96% | 19.00% |
Ultra-Sortformer requires Python + NVIDIA NeMo and cannot run on-device. The Hugging Face model (devsy0117/ultra_diar_streaming_sortformer_8spk_v1) is in principle convertible to CoreML for on-device use. Speech-swift's SortformerDiarizer uses a related NVIDIA Sortformer model (CoreML-converted) and showed 100% speaker count accuracy on both test files.
Note on Qwen3-ASR / MLX: The MLX backend failed in this benchmark run with a Metal library error (
Failed to load the default metallib). Based on the PersonaPlexDemo benchmarks (M2 Max), Qwen3-ASR achieves ~0.2 s transcription latency and supports 52 languages including German. Results from the Qwen3-ASR engine will be available once the Metal environment is restored.
PersonaPlexDemo demonstrates a full-duplex speech-to-speech voice assistant using:
Important: PersonaPlex is English-only — trained on English conversational data only. It is not suitable for German transcription or response. The Qwen3-ASR front-end, however, works for German independently.
| Metric | Value (M2 Max) |
|---|---|
| Model load | ~10 s (cached) |
| Warmup | ~15 s (first), ~3 s (cached) |
| RTF | ~0.94 (real-time) |
| Max response | ~6 s (75 steps) |
| ASR latency | ~0.2 s |
For a German-capable voice assistant, the recommended path is: Qwen3-ASR (German input) + Sortformer (diarization) + future German speech-to-speech model.
Pyannote's agglomerative clustering pipeline uses a cosine similarity threshold tuned on English VoxCeleb/CALLHOME data. German conversational speech has different prosodic patterns and overlapping speech segments, causing the default threshold to split single speakers into multiple clusters.
Sortformer uses an end-to-end neural architecture (trained by NVIDIA) that jointly learns segmentation and speaker identity — no clustering threshold to tune. On the German legal recordings tested here it achieved 100% speaker count accuracy vs. 0% for both Pyannote variants.
If you need Pyannote for specific reasons (e.g., partial overlap handling), lower the clustering threshold:
// In MLXDiarizer.swift — tune for German conversational audio
let pipeline = try await DiarizationPipeline.fromPretrained(
embeddingEngine: .mlx,
minClusterSize: 0.15 // default ~0.25; lower = fewer false splits
)
Full benchmark report (226 KB PDF, Typst 0.15, Oxford Blue / Academic Gold):
output/study.pdf — Compiled PDF (open directly)output/study.typ — Typst source (editable)output/study.md — Markdown summaryoutput/study.csv — Raw metrics CSVThe report includes: - ASR quality comparison (word count, WER vs Gemini GT, vocabulary recall, RTFx) - Inter-ASR agreement (cross-engine WER) - Diarization metrics (speaker count accuracy, RTFx, false speaker switches per minute) - Combined ASR × Diarizer pipeline table - PersonaPlexDemo architecture and Qwen3-ASR context - Ultra-Sortformer published benchmarks (German DER 6.70%) - Full resources & links section (papers, models, datasets) - Recommendations and resource budget table
# Re-run and regenerate PDF
swift run -c release diarization-study --asr apple-speech,parakeet --diarizers sortformer --file both
typst compile output/study.typ output/study.pdf