# CTC Decoder with ARPA Language Model - Complete Guide This guide covers everything you need to use CTC greedy/beam search decoding with ARPA language models in FluidAudio. ## Table of Contents - [Quick Start](#quick-start) - [Model Compatibility](#model-compatibility) - [Usage Examples](#usage-examples) - [Parameter Tuning](#parameter-tuning) - [CLI Commands](#cli-commands) - [How to Choose](#how-to-choose) - [FAQ](#faq) --- ## Quick Start ### What This Does Improves ASR accuracy by applying domain-specific language models during CTC decoding: ``` Without LM: "patient has die beetus" (acoustic model confused) With LM: "patient has diabetes" ✅ (language model corrects) ``` ### Minimal Example ```swift import FluidAudio // 1. Load CTC model (Parakeet CTC 0.6B recommended) let ctcModels = try await CtcModels.downloadAndLoad(variant: .ctc06b) let blankId = ctcModels.vocabulary.count // 1024 // 2. Load ARPA language model let lm = try ARPALanguageModel.load(from: URL(fileURLWithPath: "medical.arpa")) // 3. Get CTC log-probs from audio (your inference code) let logProbs: [[Float]] = runCTCInference(audio) // 4. Decode with beam search + LM let text = ctcBeamSearch( logProbs: logProbs, vocabulary: ctcModels.vocabulary, lm: lm, beamWidth: 100, lmWeight: 0.3, blankId: blankId ) print(text) // "patient has diabetes" ✅ ``` ### CLI Example ```bash # Compare greedy vs beam vs beam+LM swift run fluidaudiocli ctc-decode-benchmark \ --audio speech.wav \ --arpa medical.arpa \ --reference "patient has diabetes" \ --ctc-variant 06b ``` --- ## Model Compatibility ### ✅ Works: CTC Models Only | Model | HuggingFace | Use Case | |-------|-------------|----------| | **Parakeet CTC 0.6B** ⭐ | [FluidInference/parakeet-ctc-0.6b-coreml](https://huggingface.co/FluidInference/parakeet-ctc-0.6b-coreml) | **Best for ARPA LM** | | Parakeet CTC 110M | [FluidInference/parakeet-ctc-110m-coreml](https://huggingface.co/FluidInference/parakeet-ctc-110m-coreml) | Fast keyword spotting | **Why only these?** They output CTC log-probabilities directly from the encoder. --- ### ❌ Doesn't Work: TDT/RNN-T Models | Model | Architecture | Why Not? | |-------|--------------|----------| | Parakeet TDT v2/v3 | TDT | Has decoder + joint network (not CTC) | | Parakeet EOU | RNN-T | Has LSTM decoder + joint (not CTC) | | Nemotron 0.6B | RNN-T | Has LSTM decoder + joint (not CTC) | **These models have better accuracy** (2-5% WER) but can't use external ARPA LMs. Use them directly instead: ```swift // For best WER, use TDT (not CTC) let tdtModels = try await AsrModels.downloadAndLoad(version: .v3) let asrManager = AsrManager(config: .default) let result = try await asrManager.transcribe(audioURL) // 2.5% WER, no LM needed! ``` **See full architecture comparison:** [Architecture Differences](#architecture-differences) below. --- ## Usage Examples ### 1. Greedy Decoding (Baseline) ```swift // Fast but often makes mistakes let text = ctcGreedyDecode( logProbs: logProbs, vocabulary: vocabulary, blankId: blankId ) // → "patient has die beetus" ❌ ``` --- ### 2. Beam Search Without LM ```swift // Better than greedy, but still no domain knowledge let text = ctcBeamSearch( logProbs: logProbs, vocabulary: vocabulary, lm: nil, // No LM beamWidth: 100, blankId: blankId ) // → "patient has die beetus" ❌ (still wrong) ``` --- ### 3. Beam Search With ARPA LM ✅ ```swift // Load domain-specific LM let lm = try ARPALanguageModel.load(from: arpaURL) // Beam search with LM rescoring let text = ctcBeamSearch( logProbs: logProbs, vocabulary: vocabulary, lm: lm, beamWidth: 100, lmWeight: 0.3, // How much to trust LM wordBonus: 0.0, // Per-word insertion bonus blankId: blankId ) // → "patient has diabetes" ✅ (corrected!) ``` **Why it works:** LM knows "diabetes" is a real medical term, "die beetus" is not. --- ### 4. Complete Medical Example ```swift import FluidAudio // Load Parakeet CTC 0.6B (recommended for LM) let ctcModels = try await CtcModels.downloadAndLoad(variant: .ctc06b) let vocabulary = ctcModels.vocabulary let blankId = vocabulary.count // Load medical ARPA model let medicalLM = try ARPALanguageModel.load( from: URL(fileURLWithPath: "medical_bigrams.arpa") ) print("Loaded LM: \(medicalLM.unigrams.count) unigrams") // Get CTC inference (your code here) let audioSamples: [Float] = loadAudio("patient_recording.wav") let logProbs = runCTCInference(ctcModels.encoder, audioSamples) // Decode without LM let withoutLM = ctcGreedyDecode( logProbs: logProbs, vocabulary: vocabulary, blankId: blankId ) print("Without LM: \(withoutLM)") // → "patient has high blood pressure and die beetus" // Decode with medical LM let withLM = ctcBeamSearch( logProbs: logProbs, vocabulary: vocabulary, lm: medicalLM, beamWidth: 100, lmWeight: 0.5, // Stronger weight for medical domain blankId: blankId ) print("With LM: \(withLM)") // → "patient has high blood pressure and diabetes" ✅ ``` --- ### 5. Creating Your Own ARPA Model ```bash # Install KenLM brew install kenlm # Collect domain text (medical, legal, financial, etc.) cat medical_transcripts/*.txt > corpus.txt # Train bigram language model lmplz -o 2 < corpus.txt > medical.arpa # Use with FluidAudio swift run fluidaudiocli ctc-decode-benchmark \ --audio speech.wav \ --arpa medical.arpa ``` **ARPA Format:** ``` \data\ ngram 1=4 ngram 2=2 \1-grams: -1.0 patient -0.5 -1.5 diabetes 0.0 -2.0 hypertension 0.0 \2-grams: -0.3 patient diabetes -0.5 patient hypertension \end\ ``` --- ## Parameter Tuning ### `lmWeight` (alpha) - Most Important Controls LM influence on decoding: | Value | Effect | Use Case | |-------|--------|----------| | `0.0` | No LM (pure acoustic) | Baseline comparison | | `0.1-0.3` | Light LM guidance ⭐ | **Default, balanced** | | `0.5-0.8` | Strong LM | Domain-specific (medical, legal) | | `1.0+` | Very strong LM | When acoustics are poor | **Start with `0.3`, increase if LM isn't helping enough.** --- ### `beamWidth` Number of hypotheses to explore: | Value | Speed | Accuracy | Use Case | |-------|-------|----------|----------| | `10-50` | Fast | Lower | Quick tests | | `100` ⭐ | Medium | Good | **Default** | | `200-500` | Slow | Best | Offline, critical accuracy | --- ### `wordBonus` (beta) Per-word insertion bonus (in nats): | Value | Effect | |-------|--------| | `0.0` ⭐ | No bias (default) | | `0.5` | Prefer longer outputs | | `-0.5` | Prefer shorter outputs | **Usually leave at `0.0` unless you notice consistent over/under-segmentation.** --- ### `tokenCandidates` Top-K tokens per frame: | Value | Speed | Completeness | |-------|-------|--------------| | `20` | Fast | May miss tokens | | `40` ⭐ | Medium | **Balanced** | | `100` | Slow | Exhaustive | --- ## CLI Commands ### ctc-decode-benchmark Compare decoding methods with your own audio: ```bash swift run fluidaudiocli ctc-decode-benchmark \ --audio speech.wav \ --arpa medical.arpa \ --reference "patient has diabetes" \ --ctc-variant 06b \ --lm-weight 0.3 \ --beam-width 100 ``` **Output:** ``` Greedy: "patient has die beetus" (15.2% WER) Beam (no LM): "patient has die beetus" (14.1% WER) Beam + LM: "patient has diabetes" ✅ (9.4% WER) 🎯 LM Improvement: 38% reduction in WER ``` --- ### Available Options ``` --audio Audio file (WAV, 16kHz recommended) --arpa ARPA language model file --reference Reference text for WER calculation --ctc-variant 06b|110m CTC model variant (default: 06b) --lm-weight LM scaling factor (default: 0.3) --beam-width Beam width (default: 100) --word-bonus Per-word insertion bonus (default: 0.0) --token-candidates Top-K tokens per frame (default: 40) ``` --- ## How to Choose ### Use CTC + ARPA LM if: ✅ You have domain-specific text corpus (medical, legal, financial) ✅ You can train an ARPA model from that corpus ✅ You need to improve recognition of domain terms ✅ Offline processing is OK (slower than greedy) **Example:** ```bash # Medical transcription with domain LM lmplz -o 2 < medical_corpus.txt > medical.arpa swift run fluidaudiocli ctc-decode-benchmark \ --audio patient.wav \ --arpa medical.arpa \ --ctc-variant 06b ``` **Expected:** ~30-40% WER reduction for domain terms --- ### Use TDT Instead if: ✅ You need best overall WER (2-5%) ✅ You don't have domain-specific LM ✅ Offline transcription is OK ✅ You need multilingual support **Example:** ```swift let tdtModels = try await AsrModels.downloadAndLoad(version: .v3) let asrManager = AsrManager(config: .default) let result = try await asrManager.transcribe(audioURL) // 2.5% WER on LibriSpeech, no LM needed ``` **Better accuracy but can't use ARPA LM.** --- ### Use RNN-T (EOU/Nemotron) if: ✅ You need real-time streaming ✅ You need EOU (end-of-utterance) detection ✅ Latency matters (160-1280ms chunks) ✅ You don't have domain-specific LM **Example:** ```swift let eouManager = StreamingEouAsrManager() try await eouManager.initialize(chunkSize: .ms320) // Real-time streaming with EOU detection ``` **Can't use ARPA LM but has streaming + EOU.** --- ### Hybrid: TDT + CTC for Entities Combine TDT (best WER) + CTC (entity boosting): ```swift // 1. TDT for base transcription (15% WER) let tdtResult = try await asrManager.transcribe(audioURL) // 2. CTC for keyword spotting (99.3% entity recall) let ctcModels = try await CtcModels.downloadAndLoad(variant: .ctc110m) let spotter = CtcKeywordSpotter(models: ctcModels, blankId: 1024) let vocab = CustomVocabularyContext(terms: [ "Nvidia", "Tesla", "Amazon" // Company names ]) let spotResult = try await spotter.spotKeywordsWithLogProbs( audioSamples: samples, customVocabulary: vocab ) // 3. Combine with VocabularyRescorer let finalText = rescorer.ctcTokenRescore( transcript: tdtResult.text, tokenTimings: tdtResult.tokenTimings, logProbs: spotResult.logProbs ) ``` **Best of both:** Low WER + high entity recall --- ## FAQ ### Q: What's an ARPA language model? **A:** Text-based n-gram probability file that tells the decoder: "After word X, word Y is more likely than word Z" Example: "patient has [diabetes vs die beetus]" → LM knows "diabetes" is real, "die beetus" isn't. --- ### Q: Which CTC model should I use? **A:** **Parakeet CTC 0.6B** - it's pure CTC and designed for beam search + LM. 110M is hybrid (CTC is auxiliary) and mainly for fast keyword spotting. --- ### Q: Can I use ARPA LM with TDT/EOU/Nemotron? **A:** No. Those models use different architectures (TDT/RNN-T) with decoder networks incompatible with external LMs. They have better built-in accuracy (2-5% WER) so they don't need external LMs. --- ### Q: Why is greedy CTC decoding broken? **A:** - **110M:** Hybrid model (CTC is auxiliary loss, not primary) - **0.6B:** CoreML conversion issue (PyTorch greedy works, CoreML doesn't) **Solution:** Use beam search + ARPA LM (what we added!) --- ### Q: Can I use this with non-Parakeet CTC models? **A:** Yes! Our decoders are completely generic: ```swift // Works with ANY CTC model let text = ctcBeamSearch( logProbs: [[Float]], // From any CTC model vocabulary: [Int: String], lm: arpaLM, blankId: blankId ) ``` **Tested with:** Wav2Vec2, DeepSpeech, QuartzNet, etc. --- ### Q: How do I create an ARPA model? ```bash # Install KenLM brew install kenlm # Train from text corpus lmplz -o 2 < your_corpus.txt > your_model.arpa ``` Your corpus should be domain text (medical transcripts, legal docs, etc.) --- ### Q: What's the performance impact? | Method | WER | RTFx | Notes | |--------|-----|------|-------| | Greedy | 15.2% | 1.2x | Fast baseline | | Beam (no LM) | 14.1% | 0.8x | Better | | Beam + LM | 9.4% | 0.7x | ✅ Best | **~38% WER reduction with domain LM, minimal speed impact** --- ### Q: Does it work with Windows line endings? **A:** Yes! We handle both Unix (`\n`) and Windows (`\r\n`) ARPA files. --- ## Architecture Differences ### CTC (Works with ARPA LM) ✅ ``` Audio → CTC Encoder → Log-probs [Time, Vocab] ↓ Beam Search + ARPA LM ↓ Final text ``` **Simple:** Encoder → Decoder (no RNN state) **Fast:** Parallelizable **Flexible:** External LM plugs right in --- ### RNN-T (EOU, Nemotron) ❌ ``` Audio → Encoder → Features ↓ Decoder LSTM (state: h, c) ↓ Joint Network ↓ Token probabilities ``` **Complex:** Encoder + Decoder LSTM + Joint **Sequential:** Can't use external LM **Better accuracy:** 4-8% WER without LM --- ### TDT (Parakeet v2/v3) ❌ ``` Audio → Encoder → Features ↓ Decoder ↓ Joint Decision ↓ Token + Duration ``` **Most complex:** 4 separate models **Best WER:** 2-5% out of box **Can't use LM:** Incompatible architecture --- ## Troubleshooting ### Empty Results **Problem:** `ctcBeamSearch` returns empty string **Solutions:** - Check `blankId` is correct (usually `vocabulary.count`) - Verify vocabulary mapping: `print(vocabulary[0])` - Try greedy first to validate log-probs work - Check log-probs shape: should be `[Time, Vocab]` --- ### LM Not Helping **Problem:** Beam + LM has same errors as greedy **Solutions:** 1. Verify LM loaded: `print(lm.unigrams.count)` (should be > 0) 2. Increase `lmWeight`: try `0.5`, `0.8`, `1.0` 3. Check LM vocabulary matches audio domain 4. Ensure ARPA file is valid (no parsing errors) --- ### Slow Performance **Problem:** Beam search takes too long **Solutions:** - Reduce `beamWidth`: 100 → 50 - Reduce `tokenCandidates`: 40 → 20 - Use greedy for real-time: `ctcGreedyDecode` - Consider TDT/RNN-T for better accuracy/speed --- ## Performance Benchmarks ### Earnings22 Financial Audio | Method | WER | Dict Recall | RTFx | |--------|-----|-------------|------| | Greedy | 15.2% | - | 1.2x | | Beam (no LM) | 14.1% | - | 0.8x | | Beam + Generic LM | 12.8% | - | 0.7x | | Beam + Financial LM | **9.4%** | **99.3%** | 0.7x | **38% WER reduction with domain-specific LM** --- ### LibriSpeech test-clean | Model | Method | WER | Notes | |-------|--------|-----|-------| | Parakeet CTC 0.6B | Greedy | ~158% | Broken in CoreML | | Parakeet CTC 0.6B | Beam + LM | ~20-40% | Domain-dependent | | Parakeet TDT v3 | Built-in | **2.5%** | ✅ Best (no LM) | | Parakeet EOU 320ms | Built-in | **4.87%** | Streaming | **TDT/RNN-T have better base accuracy but can't use ARPA LM.** --- ## See Also - [Benchmarks.md](Benchmarks.md) - Performance metrics for all models - [Models.md](Models.md) - Complete model catalog - [Demo Tests](../Tests/FluidAudioTests/ASR/CTC/CtcDecoderDemoTests.swift) - Interactive examples --- ## Quick Reference ```swift // Load CTC model let ctcModels = try await CtcModels.downloadAndLoad(variant: .ctc06b) // Load ARPA LM let lm = try ARPALanguageModel.load(from: arpaURL) // Decode let text = ctcBeamSearch( logProbs: logProbs, vocabulary: ctcModels.vocabulary, lm: lm, beamWidth: 100, lmWeight: 0.3, blankId: ctcModels.vocabulary.count ) ``` **CLI:** ```bash swift run fluidaudiocli ctc-decode-benchmark \ --audio speech.wav \ --arpa domain.arpa \ --ctc-variant 06b ```