| --- |
| language: |
| - vi |
| license: apache-2.0 |
| tags: |
| - automatic-speech-recognition |
| - vietnamese |
| - zipformer |
| - transducer |
| - rnnt |
| - icefall |
| - meeting |
| library_name: icefall |
| base_model: actableai/zipformer-rnnt-v2 |
| datasets: |
| - actableai/vi-meeting-soniox-wer16 |
| metrics: |
| - wer |
| pipeline_tag: automatic-speech-recognition |
| --- |
| |
| # zipformer-rnnt-v3 |
|
|
| Vietnamese **Zipformer2 transducer (RNNT)** fine-tuned for **Actable meeting audio**, starting from |
| [`actableai/zipformer-rnnt-v2`](https://huggingface.co/actableai/zipformer-rnnt-v2) and trained on |
| [`actableai/vi-meeting-soniox-wer16`](https://huggingface.co/datasets/actableai/vi-meeting-soniox-wer16) |
| with **Soniox transcripts as golden labels**. |
|
|
| | | | |
| |---|---| |
| | **Base** | `actableai/zipformer-rnnt-v2` (policy-domain Zipformer, BPE-3000) | |
| | **Fine-tune data** | Meeting segments with WER(Soniox, baseline Qwen) ≤ 16% | |
| | **Labels** | Soniox `text` only (mixed-case; not uppercased) | |
| | **Params** | ~70.7 M (transducer; no CTC head in this fine-tune) | |
| | **Sample rate** | 16 kHz, 80-dim log-Mel fbank (on-the-fly) | |
|
|
| --- |
|
|
| ## Performance |
|
|
| ### Meeting dev (Soniox references) |
|
|
| Eval set: `session_soniox_dev.jsonl` → 5,453 utterances, ~13.8 h |
| Decoding: **greedy search**, icefall `greedy_search_batch` |
| Text norm for WER: lowercase, strip punctuation / bracket tags, collapse whitespace |
| Checkpoint: `best-valid-loss.pt` using **`model_avg`** weights |
| |
| | Model | Meeting-dev WER | Errors / words | |
| |-------|----------------:|---------------:| |
| | **v2 baseline** (`actableai/zipformer-rnnt-v2`) | **31.07%** | 56,369 / 181,450 | |
| | **v3 (this model)** | **20.71%** | 37,580 / 181,450 | |
| | **Δ absolute** | **−10.36** | — | |
| | **Δ relative** | **−33.3%** | — | |
| |
| > Measured 2026-07-27 with `eval_zipformer_meeting_wer16.py` (full meeting dev). |
| |
| ### Validation RNNT loss (during fine-tune) |
| |
| Loss on meeting dev CutSet at the start of each epoch (icefall validation): |
| |
| | Epoch | Valid loss | Simple | Pruned | |
| |------:|-----------:|-------:|-------:| |
| | 1 | 0.4787 | 0.4529 | 0.2577 | |
| | 2 | 0.3846 | 0.3782 | 0.1805 | |
| | 3 | 0.3605 | 0.3684 | 0.1725 | |
| | 4 | 0.3478 | 0.3633 | 0.1662 | |
| | 5 | 0.3433 | 0.3604 | 0.1631 | |
| | 6 | 0.3416 | 0.3589 | 0.1622 | |
| | 7 | 0.3398 | 0.3578 | 0.1609 | |
| | 8 | 0.3385 | 0.3565 | 0.1603 | |
| | 9 | 0.3369 | 0.3558 | 0.1590 | |
| | **10 (best)** | **0.3362** | 0.3552 | 0.1586 | |
|
|
| - Best valid loss: **0.3362** @ epoch 10 |
| - Best train loss: **0.2452** @ epoch 10 |
| - Wall time: ~1 h 38 m (single NVIDIA A100 40GB) |
|
|
| --- |
|
|
| ## Training details |
|
|
| ### Data |
|
|
| | Split | Segments | Hours | Source | |
| |-------|--------:|------:|--------| |
| | train | 22,693 | ~68.1 | `actableai/vi-meeting-soniox-wer16` (= local `session_soniox_train_filtered.jsonl`) | |
| | dev | 5,453 | ~13.8 | `session_soniox_dev.jsonl` (Soniox labels) | |
|
|
| - **Filter:** keep train segments where WER(Soniox ref, baseline Qwen hyp) ≤ **16%** |
| - **Labels:** Soniox field `text` only — never Qwen hypotheses |
| - **Audio:** 16 kHz mono session WAVs (`full_session.wav` + start/end offsets) |
| - **Mix / replay:** none (`use_mux=0`) — pure meeting wer16 |
| - **Utt duration filter:** 0.3–30 s |
|
|
| ### Optimization |
|
|
| | Knob | Value | |
| |------|--------| |
| | Recipe | icefall Zipformer2 `finetune.py` | |
| | Init | load `encoder,encoder_embed,decoder,joiner,simple_am_proj,simple_lm_proj` from v2 | |
| | CTC | **disabled** (`use_ctc=0`) — joint CTC FT was unstable (CTC bias grad explosion under fp16) | |
| | Optimizer | ScaledAdam + Eden LR schedule | |
| | `base_lr` | 5e-4 | |
| | Epochs | 10 | |
| | Batching | `max_duration=300` s / batch, dynamic bucketing | |
| | Precision | fp16 | |
| | Features | on-the-fly 80-dim Kaldi fbank | |
| | SpecAugment | on (time-warp factor 80) | |
| | MUSAN | off | |
| | Speed perturb | off | |
| | Seed | 42 | |
| | Hardware | 1× A100-SXM4-40GB | |
| | Global steps | ~8,674 | |
|
|
| ### Architecture (unchanged from v2) |
|
|
| | Param | Value | |
| |-------|--------| |
| | `num_encoder_layers` | 2,2,3,4,3,2 | |
| | `encoder_dim` | 192,256,384,512,384,256 | |
| | `downsampling_factor` | 1,2,4,8,4,2 | |
| | `feedforward_dim` | 512,768,1024,1536,1024,768 | |
| | `num_heads` | 4,4,4,8,4,4 | |
| | `cnn_module_kernel` | 31,31,15,15,15,31 | |
| | `encoder_unmasked_dim` | 192,192,256,256,256,192 | |
| | `decoder_dim` / `joiner_dim` | 512 / 512 | |
| | Vocab | BPE-3000 (mixed-case Vietnamese; same as v2) | |
|
|
| --- |
|
|
| ## Files |
|
|
| | File | Description | |
| |------|-------------| |
| | `best-valid-loss.pt` | Best icefall checkpoint (`model` + `model_avg` + train metadata) | |
| | `bpe.model` | SentencePiece BPE-3000 (shared with v2) | |
| | `tokens.txt` | Token id map for sherpa / icefall | |
| | `config.json` | Architecture + training metadata | |
| | `train_zipformer_meeting_wer16.sh` | Launch script used for this run | |
| | `prep_lhotse_meeting_wer16.py` | Lhotse CutSet prep (Soniox labels) | |
| | `eval_zipformer_meeting_wer16.py` | Greedy WER eval helper | |
|
|
| --- |
|
|
| ## Usage (icefall) |
|
|
| ```python |
| import torch, sys |
| sys.path.insert(0, "/path/to/icefall") |
| sys.path.insert(0, "/path/to/zipformer_work") # finetune.py / model defs |
| |
| from finetune import get_model, get_params, add_model_arguments |
| from beam_search import greedy_search_batch |
| import argparse, sentencepiece as spm |
| |
| parser = argparse.ArgumentParser() |
| add_model_arguments(parser) |
| params = get_params() |
| params.update(vars(parser.parse_args([]))) |
| params.encoder_dim = "192,256,384,512,384,256" |
| params.num_encoder_layers = "2,2,3,4,3,2" |
| params.downsampling_factor = "1,2,4,8,4,2" |
| params.feedforward_dim = "512,768,1024,1536,1024,768" |
| params.num_heads = "4,4,4,8,4,4" |
| params.cnn_module_kernel = "31,31,15,15,15,31" |
| params.encoder_unmasked_dim = "192,192,256,256,256,192" |
| params.decoder_dim = 512 |
| params.joiner_dim = 512 |
| params.causal = False |
| params.vocab_size = 3000 |
| params.blank_id = 0 |
| params.context_size = 2 |
| params.use_transducer = True |
| params.use_ctc = False |
| |
| model = get_model(params) |
| ckpt = torch.load("best-valid-loss.pt", map_location="cpu") |
| state = ckpt.get("model_avg") or ckpt["model"] |
| model.load_state_dict(state, strict=False) |
| model.eval().cuda() |
| |
| sp = spm.SentencePieceProcessor() |
| sp.load("bpe.model") |
| |
| # feature: (1, T, 80) float32 fbank; feature_lens: (1,) int |
| with torch.no_grad(): |
| encoder_out, encoder_out_lens = model.forward_encoder(feature, feature_lens) |
| token_ids = greedy_search_batch(model, encoder_out, encoder_out_lens) |
| text = sp.decode(token_ids[0]) |
| print(text) |
| ``` |
|
|
| --- |
|
|
| ## Notes / limitations |
|
|
| - Optimized for **Vietnamese meeting / conversational** audio; not re-evaluated on open-domain policy or YouTube suites in this card. |
| - Greedy decoding only in the reported WER; beam search may improve further. |
| - Transcripts are **Soniox** pseudo-labels (filtered), not human gold. |
| - CTC head from v2 was **not** fine-tuned / not present in the shipped transducer-only graph. |
|
|
| ## Changelog |
|
|
| | Date | Event | |
| |------|--------| |
| | 2026-07-27 | Fine-tune v2 → meeting wer16, 10 epochs; valid loss 0.479 → 0.336 | |
| | 2026-07-27 | Meeting-dev WER 31.07% (v2) → **20.71%** (v3); push `actableai/zipformer-rnnt-v3` | |
|
|