Optimized variants of
UsefulSensors/moonshine-streaming-tiny , a 34M parameter streaming ASR model designed for real-time, on-device English speech recognition.
├── onnx_int8/ # ONNX INT8 quantized (recommended for CPU)
│ ├── encoder_model_int8.onnx # 9.8 MB
│ ├── decoder_model_int8.onnx # 36 MB
│ ├── decoder_with_past_model_int8.onnx # 32 MB
│ ├── tokenizer.json
│ ├── config.json
│ └── quantize_config.json
├── onnx/ # ONNX FP32
│ ├── encoder_model.onnx + .data
│ ├── decoder_model.onnx + .data
│ ├── decoder_with_past_model.onnx + .data
│ └── ...
└── fp16/ # FP16 SafeTensors (for GPU)
├── model.safetensors # 88.1 MB
├── config.json
└── tokenizer.json
1 import numpy as np
2 import onnxruntime as ort
3 from tokenizers import Tokenizer
4
5 MODEL_DIR = "onnx_int8" # or download from this repo
6 BOS , EOS = 1 , 2
7
8 # Load models
9 opts = ort . SessionOptions ( )
10 opts . intra_op_num_threads = 4
11 providers = [ "CPUExecutionProvider" ]
12
13 encoder = ort . InferenceSession ( f" { MODEL_DIR } /encoder_model_int8.onnx" , opts , providers = providers )
14 decoder = ort . InferenceSession ( f" { MODEL_DIR } /decoder_model_int8.onnx" , opts , providers = providers )
15 decoder_past = ort . InferenceSession ( f" { MODEL_DIR } /decoder_with_past_model_int8.onnx" , opts , providers = providers )
16 tokenizer = Tokenizer . from_file ( f" { MODEL_DIR } /tokenizer.json" )
17
18 # Prepare audio (16kHz float32, padded to multiple of 80 samples)
19 audio = np . random . randn ( 16000 * 5 ) . astype ( np . float32 ) # replace with real audio
20 remainder = len ( audio ) % 80
21 if remainder :
22 audio = np . pad ( audio , ( 0 , 80 - remainder ) )
23
24 audio_input = audio [ np . newaxis , : ]
25 attention_mask = np . ones_like ( audio_input , dtype = np . int64 )
26
27 # Encode audio
28 ( enc_out , ) = encoder . run ( None , {
29 "input_values" : audio_input ,
30 "attention_mask" : attention_mask ,
31 } )
32
33 # First decode step
34 outs = decoder . run ( None , {
35 "decoder_input_ids" : np . array ( [ [ BOS ] ] , dtype = np . int64 ) ,
36 "encoder_hidden_states" : enc_out ,
37 } )
38 logits , past_kvs = outs [ 0 ] , outs [ 1 : ]
39 token = int ( np . argmax ( logits [ 0 , - 1 , : ] ) )
40
41 # Build KV cache mapping
42 dec_out_names = [ o . name for o in decoder . get_outputs ( ) ] [ 1 : ]
43 past_in_names = { i . name for i in decoder_past . get_inputs ( ) } - { "decoder_input_ids" , "encoder_hidden_states" }
44
45 kv_dict = { }
46 for name , tensor in zip ( dec_out_names , past_kvs ) :
47 mapped = name . replace ( "present_" , "past_" , 1 )
48 if mapped in past_in_names :
49 kv_dict [ mapped ] = tensor
50
51 # Autoregressive decode loop
52 past_out_names = [ o . name for o in decoder_past . get_outputs ( ) ] [ 1 : ]
53 tokens = [ token ]
54
55 for _ in range ( 255 ) :
56 if token == EOS :
57 break
58 inputs = {
59 "decoder_input_ids" : np . array ( [ [ token ] ] , dtype = np . int64 ) ,
60 "encoder_hidden_states" : enc_out ,
61 }
62 inputs . update ( kv_dict )
63 outs = decoder_past . run ( None , inputs )
64 token = int ( np . argmax ( outs [ 0 ] [ 0 , - 1 , : ] ) )
65 tokens . append ( token )
66
67 kv_dict = { }
68 for name , tensor in zip ( past_out_names , outs [ 1 : ] ) :
69 mapped = name . replace ( "present_" , "past_" , 1 )
70 if mapped in past_in_names :
71 kv_dict [ mapped ] = tensor
72
73 text = tokenizer . decode ( tokens )
74 print ( text )
1 from transformers import MoonshineStreamingForConditionalGeneration , AutoProcessor
2 import torch
3
4 model = MoonshineStreamingForConditionalGeneration . from_pretrained (
5 "felixem/moonshine-streaming-tiny-optimized" ,
6 subfolder = "fp16" ,
7 torch_dtype = torch . float16 ,
8 ) . to ( "cuda" )
9
10 processor = AutoProcessor . from_pretrained (
11 "felixem/moonshine-streaming-tiny-optimized" ,
12 subfolder = "fp16" ,
13 )
14
15 # Process audio
16 inputs = processor ( audio_array , return_tensors = "pt" , sampling_rate = 16000 )
17 inputs = { k : v . to ( "cuda" , torch . float16 ) for k , v in inputs . items ( ) }
18
19 generated_ids = model . generate ( ** inputs , max_new_tokens = 128 )
20 text = processor . decode ( generated_ids [ 0 ] , skip_special_tokens = True )
1 import torch
2 from transformers import MoonshineStreamingForConditionalGeneration , AutoProcessor
3
4 model = MoonshineStreamingForConditionalGeneration . from_pretrained (
5 "UsefulSensors/moonshine-streaming-tiny"
6 ) . eval ( )
7
8 # Quantize Linear layers to INT8
9 model = torch . quantization . quantize_dynamic (
10 model , { torch . nn . Linear } , dtype = torch . qint8
11 )
12
13 processor = AutoProcessor . from_pretrained ( "UsefulSensors/moonshine-streaming-tiny" )
14 inputs = processor ( audio_array , return_tensors = "pt" , sampling_rate = 16000 )
15 generated_ids = model . generate ( ** inputs , max_new_tokens = 128 )
16 text = processor . decode ( generated_ids [ 0 ] , skip_special_tokens = True )
Based on the
Edge-ASR paper (Table 14), INT8 quantization on Moonshine Tiny has negligible WER impact:
INT8 is the sweet spot for Moonshine Tiny — virtually no accuracy loss with ~50% model size reduction.
1 @article{kudlur2025moonshine,
2 title={Moonshine v2: Ergodic Streaming Encoder ASR},
3 author={Kudlur, Manjunath and King, Evan and Wang, James and Warden, Pete},
4 journal={arXiv preprint arXiv:2602.12241},
5 year={2025}
6 }