CoolFace
Modelpublic

actableai/zipformer-rnnt-v3

sourceHugging Faceapache-2.0updated 2mo agoView on Hugging Face
0likes6downloads
Model Card

zipformer-rnnt-v3

Vietnamese Zipformer2 transducer (RNNT) fine-tuned for Actable meeting audio, starting from `actableai/zipformer-rnnt-v2` and trained on `actableai/vi-meeting-soniox-wer16` with Soniox transcripts as golden labels.

Baseactableai/zipformer-rnnt-v2 (policy-domain Zipformer, BPE-3000)
Fine-tune dataMeeting segments with WER(Soniox, baseline Qwen) ≤ 16%
LabelsSoniox text only (mixed-case; not uppercased)
Params~70.7 M (transducer; no CTC head in this fine-tune)
Sample rate16 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

ModelMeeting-dev WERErrors / 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):

EpochValid lossSimplePruned
10.47870.45290.2577
20.38460.37820.1805
30.36050.36840.1725
40.34780.36330.1662
50.34330.36040.1631
60.34160.35890.1622
70.33980.35780.1609
80.33850.35650.1603
90.33690.35580.1590
10 (best)0.33620.35520.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

SplitSegmentsHoursSource
train22,693~68.1actableai/vi-meeting-soniox-wer16 (= local session_soniox_train_filtered.jsonl)
dev5,453~13.8session_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

KnobValue
Recipeicefall Zipformer2 finetune.py
Initload encoder,encoder_embed,decoder,joiner,simple_am_proj,simple_lm_proj from v2
CTCdisabled (use_ctc=0) — joint CTC FT was unstable (CTC bias grad explosion under fp16)
OptimizerScaledAdam + Eden LR schedule
base_lr5e-4
Epochs10
Batchingmax_duration=300 s / batch, dynamic bucketing
Precisionfp16
Featureson-the-fly 80-dim Kaldi fbank
SpecAugmenton (time-warp factor 80)
MUSANoff
Speed perturboff
Seed42
Hardware1× A100-SXM4-40GB
Global steps~8,674

Architecture (unchanged from v2)

ParamValue
num_encoder_layers2,2,3,4,3,2
encoder_dim192,256,384,512,384,256
downsampling_factor1,2,4,8,4,2
feedforward_dim512,768,1024,1536,1024,768
num_heads4,4,4,8,4,4
cnn_module_kernel31,31,15,15,15,31
encoder_unmasked_dim192,192,256,256,256,192
decoder_dim / joiner_dim512 / 512
VocabBPE-3000 (mixed-case Vietnamese; same as v2)

Files

FileDescription
best-valid-loss.ptBest icefall checkpoint (model + model_avg + train metadata)
bpe.modelSentencePiece BPE-3000 (shared with v2)
tokens.txtToken id map for sherpa / icefall
config.jsonArchitecture + training metadata
train_zipformer_meeting_wer16.shLaunch script used for this run
prep_lhotse_meeting_wer16.pyLhotse CutSet prep (Soniox labels)
eval_zipformer_meeting_wer16.pyGreedy 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

DateEvent
2026-07-27Fine-tune v2 → meeting wer16, 10 epochs; valid loss 0.479 → 0.336
2026-07-27Meeting-dev WER 31.07% (v2) → 20.71% (v3); push actableai/zipformer-rnnt-v3