# Finnish ASR: Canary-v2 Finetuning & Progress This document provides a high-level overview of our Finnish ASR finetuning process, model architecture, and current progress for the Data Science team. --- ## πŸ“Š Project Overview Our goal is to adapt NVIDIA's **Canary-v2** (a 1-billion parameter multilingual model) for high-accuracy Finnish Automatic Speech Recognition (ASR). We leverage four diverse datasets to ensure robustness across different domains and speaking styles. --- ## πŸ—οΈ Model Architecture Canary-v2 is an **Attention-Encoder-Decoder (AED)** model that utilizes the **Fast-Conformer** architecture. This design allows for efficient processing of long audio sequences while maintaining high accuracy. ```mermaid graph TD A[Audio Input] -->|Preprocessing| B[Mel Spectrogram] subgraph TrainingBlock [Finetuned Components] direction TB subgraph Encoder [Encoder: Acoustic Modeling] C1[Convolutional Subsampling] -->|Downsample| C2[Conformer Blocks] C2 -->|Latent Features| C_Out[Acoustic Latents] end subgraph Decoder [Decoder: Language Modeling] D1[Masked Self-Attention] --> D2[Cross-Attention] D2 --> D3[Feed Forward] D3 --> D_Out[Text Generation] end end B -->|Input| C1 P[Input Prompts:
Lang, Task, PnC] -->|Conditioning| D1 C_Out -->|Acoustic Context| D2 D_Out -->|Output| E[Finnish Text] %% Styling style TrainingBlock fill:#f0f7ff,stroke:#0052cc,stroke-width:3px,stroke-dasharray: 5 5 style A fill:#ffffff,stroke:#333,stroke-width:2px style B fill:#ffffff,stroke:#333,stroke-width:2px style P fill:#ffffff,stroke:#333,stroke-width:2px style E fill:#e6ffed,stroke:#28a745,stroke-width:2px style Encoder fill:#ffffff,stroke:#0052cc,stroke-width:1px style Decoder fill:#ffffff,stroke:#0052cc,stroke-width:1px ``` ### Component Roles & Finetuning: - **Highlighted Area (Blue Dashed Box)**: This represents the core weights of the **Canary-v2** model. During our finetuning, we update the parameters in both the **Encoder** and **Decoder** to specifically recognize Finnish phonemes and grammar. - **Mel Spectrogram**: The "Vision" stage. It turns raw audio waves into a structured 2D representation of sound frequencies over time. - **Fast-Conformer Encoder**: The "Acoustic Processor." We finetuned this to understand the unique sounds of the Finnish language (like double vowels and consonants). - **Input Prompts**: The "Context Injector." These are the same color as other inputs because they are part of the model's standard input pipeline, telling it: "Act as a Finnish ASR system." - **Attention-Decoder**: The "Linguistic Brain." We finetuned this to map the Finnish sounds from the encoder into grammatically correct Finnish text, guided by the prompts. --- ## πŸ”„ Finetuning Workflow Our pipeline is fully automated, from data ingestion to multi-dataset evaluation. ```mermaid graph TD subgraph DataPrep [Data Preparation] D1[CSS10 Finnish] --> P[Unified Processing Script] D2[FLEURS Finnish] --> P D3[VoxPopuli Finnish] --> P D4[Common Voice v24] --> P P --> M1[train_manifest.json] P --> M2[eval_fleurs.json] P --> M3[eval_common_voice.json] P --> M4[eval_css10.json] P --> M5[eval_voxpopuli.json] end subgraph Training [Canary-v2 Finetuning] M1 --> T[NVIDIA NeMo Trainer] CM[nvidia/canary-1b-v2] --> T T --> CK[Model Checkpoints] M2 & M3 & M4 & M5 --> V[Multi-Validation] V --> W[WandB Tracking] end subgraph Inference [Post-Processing] CK --> Inf[Inference] Inf --> K[KenLM/NGPU-LM Integration] K --> R[Final ASR Output] end ``` --- ## πŸ“š Datasets We use a balanced mix of datasets to cover various audio qualities and transcript styles: | Dataset | Source | Characteristics | |---------|--------|-----------------| | **FLEURS** | Google | High-quality, diverse speakers (Benchmark) | | **Common Voice** | Mozilla | Crowdsourced, varied quality and accents | | **CSS10** | Single Speaker | Clean, high-quality audio books | | **VoxPopuli** | EU Parliament | European Parliament speeches (Formal) | --- ## πŸ“Š Training Data Analysis This section documents the composition and length distribution of our training data (from `RASMUS/canary-finnish-asr-data`, accessed 2026-02-26). ### Dataset Summary | Dataset | Samples | Mean Duration | Max Duration | Total Hours | |---------|---------|--------------|-------------|-------------| | **Common Voice v24** | 9,086 | 4.5s | 10.5s | 11.2h | | **VoxPopuli** | 8,164 | 10.1s | 50.5s | 23.0h | | **CSS10** | 3,226 | 7.7s | 20.2s | 6.9h | | **FLEURS** | 2,704 | 11.7s | 43.2s | 8.8h | | **TOTAL** | **23,180** | **7.8s** | **50.5s** | **~50h** | ### Duration Distribution (Training Set) ``` 0–5s : 33.3% (7,725 samples) β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ 5–10s : 43.7% (10,139 samples) β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ 10–15s : 15.0% (3,473 samples) β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ 15–20s : 5.4% (1,241 samples) β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ 20–30s : 2.4% (562 samples) β–ˆβ–ˆβ–ˆ >30s : 0.2% (40 samples) ``` **Key insight:** 77% of training samples are shorter than 10 seconds. The model has very little exposure to longer audio segments (only 0.2% are >30s). This has direct implications for long-form inference stability. ### Evaluation Set Durations | Eval Set | Samples | Mean Duration | Max Duration | |----------|---------|--------------|-------------| | FLEURS | 918 | 13.0s | 33.7s | | Common Voice | 1,554 | 5.1s | 10.5s | | CSS10 | 170 | 7.5s | 10.2s | | VoxPopuli | 430 | 10.6s | 47.5s | --- ## πŸ”’ Number Handling Analysis ### Live Inference Results: Base vs Finetuned (2026-02-26) We ran both models on 5 FLEURS test samples to determine each model's number output style. | # | Scenario | Reference | Base Canary-v2 | Our Finetuned | |---|----------|-----------|----------------|---------------| | 1 | Spoken "sata" (hundred) | `yli sata vuotta` | `yli 100 vuotta` ❌ | `yli 100 vuotta` ❌ | | 2 | Spoken "seitsemΓ€ntoista" (17) | `surmaten seitsemΓ€ntoista henkeΓ€` | `surmaten 17 henkeΓ€` ❌ | `surmaten seitsemΓ€ntoista henkeΓ€` βœ… | | 3 | Digits in reference (15, 2011, 2017) | `15 metriΓ€... 2011... 2017` | Correct βœ… | Correct βœ… | | 4 | Abbreviation "jKr." (AD) | `400 jKr.` | `400 jΓ€lkeen Kristuksen` | `400 jΓ€lkeen Kristuksen` | | 5 | Range "25–30" (en-dash U+2013) | `25–30 vuodella` | `25-30 vuodella` (ASCII hyphen) | `25 ⁇ 30 vuodella` ❌ UNK token | **Key findings:** 1. **Base model outputs digits.** When the speaker says "sata" (hundred) or "seitsemΓ€ntoista" (seventeen), the base Canary-v2 outputs `100` and `17`. This is NVIDIA's built-in text normalisation β€” Canary always outputs digit form for numbers. 2. **Finetuning introduced inconsistency.** Our finetuning partially reversed this: for `seitsemΓ€ntoista` the finetuned model now outputs the written word (because FLEURS training transcripts used written-out numbers), but still outputs `100` for `sata`. This inconsistency is worse than either consistent policy. 3. **En-dash produces a UNK token in the finetuned model.** The character `–` (U+2013 en-dash) in `25–30` causes the finetuned model to emit `⁇` (SentencePiece UNK). The base model degrades gracefully to an ASCII hyphen `25-30`. This is a regression introduced by finetuning β€” likely because the en-dash was absent or inconsistently encoded in our training data. 4. **Abbreviations are expanded by both models.** `jKr.` β†’ `jΓ€lkeen Kristuksen` in both β€” this is model behaviour, not a finetuning artifact. ### Policy Decision **We want digit output** (not written-out Finnish number words). The base model's behaviour is correct here. The finetuned model regressed on consistency because our FLEURS training transcripts used written-out numbers. ### Training Data Issues Found - Only **2.5% (578 / 23,180)** of training samples contain digit characters at all. - FLEURS transcripts use written-out numbers (`sata vuotta`) while VoxPopuli and Common Voice use digits. This gives the model conflicting signal. - En-dash (`–` U+2013) may be absent or mis-encoded in training manifests, causing UNK tokens at inference time. ### Action Plan: Numbers & UNK Token #### Step 1 β€” Normalise training transcripts to digit form Run a pre-processing pass on `train_manifest.json` before the next training run: - Use the Python library `num2words` with locale `fi` to convert Finnish written-out numbers to digits: e.g. `sata` β†’ `100`, `seitsemΓ€ntoista` β†’ `17`. - OR (simpler / safer): replace the FLEURS transcripts in the manifest with their **raw reference texts which already have digits** (FLEURS provides both `raw_transcription` and `transcription` columns; currently we use `raw_transcription` which has written numbers). - Target: **all numeric quantities consistently in digit form** across all four datasets. #### Step 2 β€” Fix en-dash encoding (ROOT CAUSE CONFIRMED) **Confirmed via tokenizer inspection (2026-02-26):** ```python m.tokenizer.text_to_ids("25–30") # β†’ [16053, 1125, 1128, 0, 1126, 1123] # ↑ id 0 = UNK for the en-dash! m.tokenizer.text_to_ids("25-30") # β†’ [16053, 1125, 1128, 16107, 1126, 1123] # ↑ ASCII hyphen tokenises correctly ``` - **En-dash `–` (U+2013) and em-dash `β€”` (U+2014) are NOT in the CanaryBPETokenizer vocabulary** (both map to UNK id 0). - Training data contains **85 entries with en-dash** (83 FLEURS, 2 Common Voice). During training, the en-dash in the TARGET text was encoded as UNK, so the model learned to produce UNK for the corresponding speech sounds. - **Fix: replace all `–` and `β€”` with ASCII hyphen `-` in all training transcripts** before the next training run. This is a one-line preprocessing step. ```python # In manifest preprocessing: text = text.replace('\u2013', '-').replace('\u2014', '-') ``` #### Step 3 β€” Re-evaluate after normalisation After normalising transcripts, re-run the 5-sample live inference test to verify: - `sata vuotta` audio β†’ model outputs `100 vuotta` - `seitsemΓ€ntoista` audio β†’ model outputs `17` - `25–30` audio β†’ model outputs `25-30` or `25–30` (no UNK) --- ## πŸ”ˆ Long-Form Audio: Root Cause Analysis Our test file `moo.wav` is **30 minutes** (1,800s) of continuous Finnish speech. This reveals a core gap vs. our finetuned Whisper model. ### How Canary-v2 Handles Long Audio (Natively) - NVIDIA's Canary-v2 uses **dynamic chunking** with 1-second overlap between chunks. - This is automatically triggered for audio longer than **40 seconds**. - The model was pre-trained on a 1.7M-hour multilingual corpus with this chunking strategy baked in. ### Our Current Approach (`inference_vad.py`) 1. Silero VAD detects speech segments. 2. Segments are merged into chunks up to `chunk_len` seconds (default: **15s**). 3. Each chunk is transcribed **independently** β€” no shared context between chunks. ### Root Causes of Degradation on Long-Form | Issue | Detail | |-------|--------| | **Training length mismatch** | 77% of fine-tuning data is <10s. Inference chunks at 15s are longer than nearly all training examples, creating distribution shift. | | **No cross-chunk context** | Each 15s chunk is transcribed in isolation. Canary's attention decoder has no memory of previous chunks, so topic/speaker continuity is lost at boundaries. | | **VAD vs. native chunking** | Our VAD-based approach differs from Canary's built-in dynamic chunking. The model was not fine-tuned with this chunking strategy. | | **Repetition / hallucination** | At chunk boundaries with silence or music, the decoder can loop. This is worsened when segments are near the edge of the model's training length distribution. | | **No overlap** | Without overlap between chunks, words at segment boundaries can be dropped or doubled. | ### Comparison: Canary vs. Our Finetuned Whisper on Long-Form Whisper was explicitly designed and trained for long-form audio with: - Sliding window inference with overlap - Previous-chunk text as conditioning (prompt-based context) - Timestamps for alignment Canary's AED architecture does not use previous-chunk text as input, making long-form continuity fundamentally harder to achieve without careful chunk overlap and stitching. --- ## πŸš€ Progress & Results ### Current Status: **Model Released & Repository Consolidated** We have successfully completed the finetuning, KenLM integration, and repository consolidation phases. The model and its associated language models are now hosted on Hugging Face at `RASMUS/Finnish-ASR-Canary-v2`. - **Infrastructure:** Finetuned on **RTX 6000 PRO Blackwell** (96 GB VRAM) on Verda.com platform in Finland. - **Model Suite:** Acoustic model + 3 KenLM variants (1M, 2M, 5M sentences). - **Best Performance (with KenLM 5M):** - **FLEURS:** 7.86% WER - **Common Voice:** 4.70% WER - **CSS10:** 7.07% WER - **VoxPopuli:** 11.65% WER - **Deployment:** Integrated Silero VAD-based inference for robust long-form audio processing. ### Next Steps: 1. **Long-form Tuning:** Reduce default `chunk_len` to 8–10s (closer to training distribution median) and add 0.5–1s overlap between chunks to reduce boundary artifacts. 2. **Data Quality Audit:** Fix 28 confirmed corrupted Common Voice entries where raw TSV metadata (client ID hashes, gender tags) was accidentally written into the `text` field. Audit VoxPopuli for missing capitalisation (all-lowercase transcripts despite `pnc: yes`). 3. **Number Handling:** Add Finnish-specific training data with numeric content. Consider TTS-synthesised samples covering phone numbers, years, statistics, and measurements (both digit and written-out forms paired). 4. **Long-form Training Data:** Incorporate longer audio segments: TTS synthetic long-form audio (`fbc_monolog_processed`, parliament data) into the training manifest to shift the duration distribution toward 15–30s. 5. **KenLM Refinement:** Re-train KenLM with high-quality punctuated text. Current LM trained on mixed-quality data. 6. **Advanced Evaluation:** Implement CER evaluation on non-normalised test sets to better capture punctuation/casing accuracy. 7. **Repetition Penalty:** Explore repetition penalty in decoding if chunk-level loops persist after chunk length tuning. 8. **Real-world Evaluation:** Benchmark on diverse long-form samples (podcasts, meetings, call-centre audio). --- ## πŸ—ΊοΈ Action Plan: Next Training Run This section details the concrete steps for the next finetuning iteration, based on the root-cause analysis above. ### Priority 1 β€” Fix Training Data (before re-training) #### 1a. Normalise numbers to digit form (Gemini Flash) Finnish written-out numbers in FLEURS transcripts cause the finetuned model to output inconsistent number forms. We will use the Gemini Flash API to convert all training transcripts in a single batch pass: ```python # Pseudocode β€” run once on train_manifest.json before next training import google.generativeai as genai import json genai.configure(api_key=GEMINI_API_KEY) model = genai.GenerativeModel("gemini-2.0-flash") SYSTEM_PROMPT = """You are a Finnish text normalizer. Convert any written-out Finnish numbers, ordinals, or number words in the text to digit form. Examples: "yli sata vuotta" β†’ "yli 100 vuotta" "seitsemΓ€ntoista henkeΓ€" β†’ "17 henkeΓ€" "vuonna tuhat yhdeksΓ€nsataa" β†’ "vuonna 1900" Keep all other text exactly as-is. Return only the modified text, nothing else.""" entries = [] with open('manifests/train_manifest.json') as f: for line in f: d = json.loads(line) response = model.generate_content(f"{SYSTEM_PROMPT}\n\n{d['text']}") d['text'] = response.text.strip() entries.append(d) with open('manifests/train_manifest_normalised.json', 'w') as f: for e in entries: f.write(json.dumps(e, ensure_ascii=False) + '\n') ``` Cost estimate: 23,180 entries Γ— ~50 tokens average = ~1.2M tokens. At Gemini Flash pricing (~$0.075/1M tokens input) β‰ˆ **< $0.10 total**. #### 1b. Fix en-dash UNK token (confirmed root cause) The en-dash `–` (U+2013) is NOT in the tokenizer vocabulary β€” it maps to UNK (id 0). Replace it with ASCII hyphen before training: ```python # Add to the manifest preprocessing step text = text.replace('\u2013', '-').replace('\u2014', '-') ``` This affects **85 entries** in `train_manifest.json` (83 FLEURS, 2 Common Voice). #### 1c. Fix 28 corrupted Common Voice entries Replace entries where the `text` field contains raw TSV metadata (tabs + client_id hashes). Strip everything after the first tab character. --- ### Priority 2 β€” Add Long-Form Training Data #### TTS Long-Form Dataset: `RASMUS/canary_asr_finetune_tts_long_data` | Property | Value | |----------|-------| | Size | 8.0 GB zip | | Format | FLAC audio + JSONL manifest | | Mean duration | **16.5s** (vs 7.8s in current data) | | Median duration | 15.9s | | Max duration | 25.0s | | Content | Finnish speech: lectures, podcasts, YouTube | | Segments >20s | ~25% | This dataset directly addresses the training length mismatch. Adding it will shift the duration distribution from a mean of 7.8s toward ~10–12s and significantly increase the proportion of 15–25s segments that match inference chunk lengths. **Integration plan:** ```bash # Download the dataset curl -L -H "Authorization: Bearer ${HF_TOKEN}" \ "https://huggingface.co/datasets/RASMUS/canary_asr_finetune_tts_long_data/resolve/main/canary_dataset.zip" \ -o /workspace/data/tts_long_data.zip # Extract unzip /workspace/data/tts_long_data.zip -d /workspace/data/tts_long_data/ # Apply number normalisation and dash fix to canary_manifest.jsonl # then merge with existing train_manifest_normalised.json ``` After applying number normalisation and dash fixes to the new manifest, concatenate with the existing training set. Expected combined size: ~23,180 + N (estimate 5,000–20,000+ entries depending on total dataset size). --- ### Priority 3 β€” Inference Tuning (without re-training) Even before re-training, we can improve `moo.wav` performance by adjusting `inference_vad.py`: | Parameter | Current | Recommended | |-----------|---------|-------------| | `chunk_len` | 15s | 8–10s (match training median of 7.8s) | | chunk overlap | 0s | 0.5s (reduce boundary word drops) | | `alpha` (KenLM) | 0.2 | Try 0.1–0.15 (current may over-constrain decoder) | --- ## πŸ”„ Round 2: Data Pipeline & Splits This section documents the data preparation methodology for Round 2 finetuning, including all new eval sets, the TTS integration, and the final manifest composition. ### Overview of Changes vs Round 1 | Item | Round 1 | Round 2 | |------|---------|---------| | Base model | `canary-1b-v2.nemo` | `canary-1b-v2.nemo` (fresh start) | | Training samples | 23,180 | **28,858** | | Training hours | ~50h | **75.6h** | | Mean duration | 7.8s | **9.4s** | | Max duration allowed | 20.0s | **30.0s** | | Transcripts normalised | No | **Yes (digits, dashes fixed)** | | Eval sets | 4 | **6** | ### Step 1 β€” Transcript Normalisation (`normalize_manifests.py`) All training transcripts were cleaned in two layers: **Deterministic fixes (no API call needed):** - En-dash `–` (U+2013) and em-dash `β€”` (U+2014) β†’ ASCII hyphen `-` (fixes UNK token regression) - Corrupted Common Voice entries (raw TSV metadata in `text` field) β†’ strip everything after first tab **Gemini 2.5 Flash API calls (2,586 of 23,180 entries needed conversion):** - Pre-filtered with a Finnish number-word regex so only entries that actually contain written numbers are sent to the API (cost: ~$0.62) - Written Finnish numbers converted to digit form: `sata vuotta` β†’ `100 vuotta`, `seitsemΓ€ntoista` β†’ `17` - Explicit DO NOT CONVERT rules: ordinals (`ensimmΓ€inen`, `toinen`), superlative constructions (`yksi tΓ€rkeimmistΓ€`), and `toinen` as "another/other" ### Step 2 β€” TTS Long-Form Data Integration Downloaded `RASMUS/canary_asr_finetune_tts_long_data` (4.8 GB, 6,365 entries, mean 16.4s). Aligned to NeMo training format: - Path rewritten to relative style: `data/tts_long_data/audio/{filename}` - Fields mapped: `language` β†’ `source_lang`/`target_lang`, `task: "transcription"` β†’ `taskname: "asr"`, added `pnc: "yes"` - Same Gemini normalisation pass applied (888 entries converted) ### Step 3 β€” Eval Set Construction (TTS Data) The 6,365 normalised TTS entries were split into train / eval / long-form-test: ``` All TTS entries (6,365) β”‚ β”œβ”€β”€ Long-form pool (>20s): 1,501 entries β”‚ β”œβ”€β”€ eval_long_form (sampled): 200 entries ← random.seed(42) shuffle β†’ first 200 β”‚ └── Returned to training pool: 1,301 entries β”‚ └── Medium pool (10–20s): 4,864 entries β”œβ”€β”€ eval_tts (10% hold-out): 487 entries ← stratified by duration bucket └── tts_train: 4,377 entries ``` **Why eval_long_form = 200 entries?** The original 1,501 long-form entries (>20s) had a total duration of ~9.4 hours β€” far too long to run as a validation set every epoch. At batch_size=32 on a single GPU, each validation pass over 1,501 entries takes ~25 minutes, adding 2.5h per epoch. 200 entries (β‰ˆ75 minutes of audio) provides a representative sample of the long-form distribution at reasonable cost: ~4 minutes of eval time per epoch. **eval_tts construction:** 487 entries were held out from the 10–20s duration range (10% stratified sample). This tests the model's ability to handle medium-length audio and is separate from the original 4 eval sets. ### Step 4 β€” Combined Training Manifest Final `train_manifest_combined.jsonl` composition: | Source | Entries | Notes | |--------|---------|-------| | Original train (normalised) | 23,180 | Digits + dash fix applied | | TTS train (10–20s) | 4,377 | Synthesised long-form speech | | Long-form overflow | 1,301 | >20s entries not selected for eval_long_form | | **Total** | **28,858** | Mean 9.4s, 75.6h | ### Final Eval Sets (Round 2) | Set | File | Entries | Mean Duration | Purpose | |-----|------|---------|--------------|---------| | `eval_fleurs` | `eval_fleurs.json` | 918 | 13.0s | Primary benchmark (monitored for checkpointing) | | `eval_common_voice` | `eval_common_voice.json` | 1,554 | 5.1s | Crowdsourced quality | | `eval_css10` | `eval_css10.json` | 170 | 7.5s | Clean single-speaker | | `eval_voxpopuli` | `eval_voxpopuli.json` | 430 | 10.6s | Formal/parliament speech | | `eval_tts` | `eval_tts.jsonl` | 487 | 14.5s | Medium-length TTS (new) | | `eval_long_form` | `eval_long_form.jsonl` | **200** | 22.5s | Long-form >20s sample (new) | **Checkpoint monitoring:** `val_wer` tracks FLEURS (first validation set). All 6 WERs are logged independently to WandB. ### Round 2 Training Config File: `configs/canary_finetune_finnish_v2.yaml` Key settings: - `init_from_nemo_model`: `/workspace/Finnish-ASR-Canary-v2/models/canary-1b-v2.nemo` (fresh start from base) - `max_duration`: 30.0s (up from 20.0s to include TTS segments up to 25s) - `max_steps`: 18,000 (scaled: 28,858 / 32 β‰ˆ 902 steps/epoch Γ— 20 epochs β‰ˆ 18,040) - `lr`: 1e-5, `WarmupAnnealing`, 500 warmup steps - `precision`: bf16, single GPU, `strategy: auto` --- ## πŸ› οΈ Workflow Status Details ### 1. Data Preparation - DONE - [x] Identify and inventory all 4 datasets - [x] Create unified processing script (`scripts/prepare_all_manifests.py`) - [x] Run `scripts/prepare_all_manifests.py` on devcontainer - [x] Verify manifest sample counts and audio file integrity ### 2. Configuration Setup - DONE - [x] Create Hydra training config (`configs/canary_finetune_finnish.yaml`) - [x] Configure multi-validation with 4 eval datasets - [x] Checkpoint monitors primary eval set (FLEURS) via `val_wer` - [x] All 4 eval WERs logged independently to WandB ### 3. Training - DONE - [x] Run finetuning via `run_training.sh` - [x] Monitor per-dataset WER in WandB ### 4. KenLM / NGPU-LM Language Model Integration - DONE - [x] Install KenLM tools (`install_beamsearch_decoders.sh`) - [x] Gather Finnish text (ASR transcripts + Wikipedia + mc4) - [x] Train 3 variants of KenLM (1M, 2M, 5M sentences) - [x] Evaluate with LM fusion on all 4 test sets ### 5. Repository & Long-Form Inference - IN PROGRESS - [x] Consolidate README and model metadata for Hugging Face release - [x] Upload model checkpoints and KenLM bundles to HF Hub - [x] Implement Silero VAD-based chunking for long-form audio (`inference_vad.py`) - [x] Root-cause analysis of long-form degradation vs. Whisper (see above) - [ ] Reduce `chunk_len` to 8–10s and add chunk overlap (Current Focus) - [ ] Optimize `alpha` for stability on `moo.wav` (30 min test file) ### 6. Data Quality & Advanced Evaluation - PARTIALLY DONE - [x] Fix 28 corrupted Common Voice manifest entries (raw TSV data in text field) β€” done in normalisation pass. - [x] Fix en-dash/em-dash UNK token regression β€” done in normalisation pass. - [ ] Audit VoxPopuli transcripts for all-lowercase entries (capitalisation missing). - [ ] Re-train KenLM with high-quality punctuated text. - [ ] Evaluate CER on non-normalized test sets. ### 7. Number Normalisation & UNK Token Fix - DONE - [x] Replace en-dash `–` and em-dash `β€”` with ASCII hyphen `-` in all training manifests (85 train + 70 TTS entries fixed). - [x] Use Gemini 2.5 Flash to normalise written-out Finnish numbers to digit form (2,586 API calls across train + TTS). - [ ] Re-evaluate on the 5-sample number test set after Round 2 training to verify consistency. ### 8. Long-Form Data Expansion - DONE - [x] Download `RASMUS/canary_asr_finetune_tts_long_data` (4.8 GB zip, 6,365 entries, mean 16.4s). - [x] Align TTS manifest to NeMo training format and integrate into combined training manifest. - [x] Round 2 training configured and ready to launch (see Round 2 section below). - [ ] Benchmark Round 2 model against Round 1 and finetuned Whisper on `moo.wav`. --- ## πŸ› οΈ NeMo Environment Setup This section documents the exact steps to set up a working NeMo inference/training environment, including the fixes required for the `nvcr.io/nvidia/pytorch:25.01-py3` container. ### Installation (from scratch on pytorch:25.01-py3 base image) ```bash # 1. Clone the HF model repo (contains NeMo source with patches applied) # Skip LFS to avoid downloading the 3.6 GB model during clone GIT_LFS_SKIP_SMUDGE=1 git clone \ "https://user:${HF_TOKEN}@huggingface.co/RASMUS/Finnish-ASR-Canary-v2" \ /workspace/Finnish-ASR-Canary-v2 # 2. Install NeMo in editable mode from the patched source cd /workspace/Finnish-ASR-Canary-v2/NeMo pip install -e ".[asr]" # 3. Install pinned dependencies pip install 'fsspec==2024.12.0' 'numpy<2.0' 'librosa>=0.11.0' kaldialign wandb ``` ### Required Compatibility Fixes The pytorch:25.01-py3 container ships with packages that conflict with NeMo 2.8.0rc0: ```bash # Fix 1: Downgrade lightning to the version NeMo requires (<=2.4.0) # The container ships lightning 2.4.0 but pip may upgrade it β€” pin it back. pip install "lightning==2.4.0" "pytorch-lightning==2.4.0" # Fix 2: Remove incompatible torchvision # The container's torchvision (0.20.0a0) was built against torch 2.6.0a0 (the original # container torch), but NeMo's install upgrades torch to ~2.10. torchvision then fails # on import and blocks NeMo. ASR does not need torchvision. pip uninstall -y torchvision ``` ### Downloading the Finetuned Model ```bash # Download the finetuned acoustic model (3.6 GB) curl -L \ -H "Authorization: Bearer ${HF_TOKEN}" \ "https://huggingface.co/RASMUS/Finnish-ASR-Canary-v2/resolve/main/canary-finnish.nemo" \ -o /workspace/Finnish-ASR-Canary-v2/canary-finnish.nemo # KenLM models are also LFS β€” download the 5M variant (best WER): curl -L \ -H "Authorization: Bearer ${HF_TOKEN}" \ "https://huggingface.co/RASMUS/Finnish-ASR-Canary-v2/resolve/main/kenlm_5M.nemo" \ -o /workspace/Finnish-ASR-Canary-v2/kenlm_5M.nemo ``` ### Quick Inference Smoke Test ```python import warnings; warnings.filterwarnings('ignore') from nemo.collections.asr.models import EncDecMultiTaskModel model = EncDecMultiTaskModel.restore_from( '/workspace/Finnish-ASR-Canary-v2/canary-finnish.nemo', map_location='cuda' ) model.eval() results = model.transcribe( audio=['path/to/audio.wav'], task='asr', source_lang='fi', target_lang='fi', pnc='yes' ) print(results[0].text) ``` ### Loading the Base Model (for comparison) ```python # Downloads ~3.6 GB on first run, cached in ~/.cache/huggingface/ model_base = EncDecMultiTaskModel.from_pretrained("nvidia/canary-1b-v2", map_location='cuda') ``` --- ## πŸ“ Progress Log - **2026-01-11:** Initial project setup. - **2026-02-08:** Redesigned data pipeline for 4 real datasets (CSS10, FLEURS, VoxPopuli, Common Voice). - **2026-02-10:** **Finetuning complete.** Epoch 11 reached `val_wer=0.1258` on FLEURS. - **2026-02-13:** Mermaid diagrams and project documentation for DS team. - **2026-02-18:** **KenLM benchmarks finished.** Consolidated repository structure. Applied NeMo patches for inference stability. - **2026-02-20:** **Model Released.** Release of `Finnish-ASR-Canary-v2` on HF. Implemented VAD-based inference pipeline. Currently tuning for long-form stability on `moo.wav` with various `alpha` settings (0.0 - 0.4 tested). - **2026-02-26:** **Root-cause analysis complete.** Investigated long-form gap vs. Whisper and number handling. Key findings: (1) 77% of training data is <10s, creating distribution shift at inference chunk lengths; (2) No cross-chunk context in Canary's AED architecture; (3) Only 2.5% of training samples contain digit characters β€” numbers are a known weak point; (4) 28 corrupted Common Voice entries found (TSV metadata in text field); (5) `moo.wav` test file confirmed as 30 minutes. Action plan: shorten chunk_len, add chunk overlap, fix data corruption, and plan a long-form training data expansion round. - **2026-02-26:** **Live number inference + tokenizer audit completed.** Ran base Canary-v2 vs. finetuned model on 5 FLEURS samples. Confirmed: (1) base model always outputs digits (`100`, `17`); (2) finetuned model regressed to mixed output β€” sometimes written words, sometimes digits β€” due to inconsistent training transcripts; (3) en-dash (`–`) produces UNK token `⁇` in finetuned model, base model degrades gracefully to ASCII hyphen. Policy decision: **standardise on digit output** and fix en-dash encoding in training manifests before next training run. NeMo environment setup documented (with fixes for `torchvision` and `lightning` version conflicts). TTS long-form dataset (`canary_asr_finetune_tts_long_data`, 8GB, mean 16.5s/segment) identified as key data source for next training run. Action plan for next run: (1) normalise numbers to digits via Gemini Flash API, (2) fix en-dash β†’ ASCII hyphen, (3) fix 28 corrupted CV entries, (4) add TTS long-form data. - **2026-03-01:** **Round 2 data pipeline complete.** Ran `normalize_manifests.py`: 2,586 Gemini 2.5 Flash API calls (~$0.62), 1,137 number changes in train + 888 in TTS, 85 en-dash and 28 corrupted CV entries fixed. Downloaded and extracted TTS long-form dataset (6,365 entries, 4.8 GB). Split TTS data into train (4,377), eval_tts (487, mean 14.5s), and long-form pool (1,501 entries >20s). Sampled 200 entries into `eval_long_form.jsonl` (seed 42) and returned 1,301 to training, yielding `train_manifest_combined.jsonl` (28,858 entries, 75.6h). Round 2 training config created (`configs/canary_finetune_finnish_v2.yaml`). **Training ready to launch.** - **2026-03-01:** **Training crash diagnosed and fixed.** Round 2 training ran 505 steps then crashed with CUDA `vectorized_gather_kernel index out of bounds`. Root cause: entry 14857 in `train_manifest_combined.jsonl` contained 11,247 chars of Python code (Gemini normalization returned a code block instead of a transcript for `voxpopuli_005371.wav`). When tokenized with the canary2 prompt format, the sequence far exceeded the decoder's `max_sequence_length=1024`, causing position-embedding OOB. Additionally, 4 entries in `eval_common_voice.json` had TSV metadata contamination (same v1 issue, not previously caught in the v2 eval set). Both manifests fixed. Config rewritten from full-architecture spec to minimal v1-style format (`tokenizer: update_tokenizer: false`) using `speech_to_text_finetune.py` (which restores the full model from the `.nemo` file). Training re-launched. Manifests synced to `canary-finnish-asr-data` HuggingFace dataset repo.