Add model card: usage script + measured performance (iPhone 17 Pro / M4 Max / Raspberry Pi 5)

#1
by mlboydaisuke - opened
Files changed (1) hide show
  1. README.md +236 -1
README.md CHANGED
@@ -1,6 +1,241 @@
1
  ---
2
  license: mit
 
3
  base_model:
4
  - UsefulSensors/moonshine-tiny
5
  pipeline_tag: automatic-speech-recognition
6
- ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  license: mit
3
+ library_name: litert
4
  base_model:
5
  - UsefulSensors/moonshine-tiny
6
  pipeline_tag: automatic-speech-recognition
7
+ tags:
8
+ - audio
9
+ - automatic-speech-recognition
10
+ - litert
11
+ - tflite
12
+ ---
13
+
14
+ # Moonshine Tiny — LiteRT
15
+
16
+ [Moonshine Tiny](https://huggingface.co/UsefulSensors/moonshine-tiny) is a
17
+ 27M-parameter encoder-decoder speech-recognition model from Moonshine AI
18
+ (formerly Useful Sensors), introduced in
19
+ [Moonshine: Speech Recognition for Live Transcription and Voice Commands](https://arxiv.org/abs/2410.15608).
20
+ It transcribes English speech and is designed for fast on-device inference.
21
+
22
+ This repository packages the model for [LiteRT](https://ai.google.dev/edge/litert):
23
+ a float32 model, a dynamic-range int8 model, and ahead-of-time compiled
24
+ variants for a range of MediaTek and Qualcomm SoCs so the model can run on the
25
+ device NPU.
26
+
27
+ ## Model description
28
+
29
+ Each `.tflite` file contains two signatures that together form the
30
+ transcription loop:
31
+
32
+ | Signature | Inputs | Output |
33
+ |---|---|---|
34
+ | `encode` | raw audio `[1, 80000]` float32 (5 s at 16 kHz, zero-padded) | encoder states `[1, 207, 288]` float32 |
35
+ | `decode` | states `[1, 207, 288]`, tokens `[1, 64]` int32, additive causal mask `[1, 1, 64, 64]` float32 | logits `[1, 64, 32768]` float32 |
36
+
37
+ - The audio frontend is inside the graph: the model takes a raw 16 kHz
38
+ waveform in `[-1, 1]` — no mel-spectrogram extraction is needed.
39
+ - The window is fixed at 5 seconds. Longer audio is transcribed in
40
+ consecutive 5 s windows; shorter audio is zero-padded.
41
+ - Decoding is greedy: start token `1`, EOS token `2`, at most 64 tokens per
42
+ window. The decoder re-scores the full token buffer each step (no KV
43
+ cache), so decode time grows with the number of emitted tokens.
44
+ - The tokenizer is not duplicated in this repository — load `tokenizer.json`
45
+ from the base model repository (see the script below).
46
+
47
+ ### Files
48
+
49
+ | File | Description |
50
+ |---|---|
51
+ | `moonshine_tiny_5s_f32.tflite` | float32 model (109 MB) |
52
+ | `moonshine_tiny_5s_i8.tflite` | dynamic-range int8 model (29 MB) |
53
+ | `moonshine_tiny_5s_f32_MediaTek_*.tflite` | float32 AOT-compiled for MediaTek NPUs (per SoC) |
54
+ | `moonshine_tiny_5s_f32_Qualcomm_*.tflite` | float32 AOT-compiled for Qualcomm NPUs (per SoC) |
55
+
56
+ ## How to use
57
+
58
+ **1. Install dependencies**
59
+
60
+ ```bash
61
+ pip install ai-edge-litert numpy tokenizers huggingface_hub
62
+ ```
63
+
64
+ **2. Save the script** below as `transcribe.py`:
65
+
66
+ ```python
67
+ #!/usr/bin/env python3
68
+ """Transcribe a wav file with litert-community/moonshine-tiny (LiteRT)."""
69
+ import argparse
70
+ import wave
71
+
72
+ import numpy as np
73
+ from ai_edge_litert.compiled_model import CompiledModel
74
+ from huggingface_hub import hf_hub_download
75
+ from tokenizers import Tokenizer
76
+
77
+ WINDOW_SAMPLES = 80000 # 5 s at 16 kHz
78
+ MAX_TOKENS = 64
79
+ START_TOKEN = 1
80
+ EOS_TOKEN = 2
81
+
82
+
83
+ def load_wav_16k_mono(path: str) -> np.ndarray:
84
+ """Reads a wav file as float32 mono at 16 kHz."""
85
+ with wave.open(path, "rb") as w:
86
+ rate, channels = w.getframerate(), w.getnchannels()
87
+ pcm = np.frombuffer(w.readframes(w.getnframes()), dtype=np.int16)
88
+ audio = pcm.astype(np.float32) / 32768.0
89
+ if channels > 1:
90
+ audio = audio.reshape(-1, channels).mean(axis=1)
91
+ if rate != 16000:
92
+ n = int(round(len(audio) * 16000 / rate))
93
+ audio = np.interp(
94
+ np.linspace(0, len(audio) - 1, n), np.arange(len(audio)), audio
95
+ ).astype(np.float32)
96
+ return audio
97
+
98
+
99
+ class MoonshineTiny:
100
+ """5 s window encoder/decoder with greedy decoding."""
101
+
102
+ def __init__(self, model_path: str, tokenizer_path: str):
103
+ self.model = CompiledModel.from_file(model_path)
104
+ self.tokenizer = Tokenizer.from_file(tokenizer_path)
105
+ self.encode_idx = self.model.get_signature_index("encode")
106
+ self.decode_idx = self.model.get_signature_index("decode")
107
+ # Additive causal mask: 0 on and below the diagonal, -1e9 above.
108
+ causal = np.tril(np.ones((MAX_TOKENS, MAX_TOKENS), dtype=bool))
109
+ self.mask = np.where(causal, 0.0, -1e9).astype(np.float32)[None, None]
110
+
111
+ def _transcribe_window(self, audio: np.ndarray) -> str:
112
+ """Transcribes up to 5 s of 16 kHz audio."""
113
+ buf = np.zeros((1, WINDOW_SAMPLES), dtype=np.float32)
114
+ buf[0, : len(audio)] = audio
115
+
116
+ enc_in = self.model.create_input_buffers(self.encode_idx)
117
+ enc_out = self.model.create_output_buffers(self.encode_idx)
118
+ enc_in[0].write(buf)
119
+ self.model.run_by_index(self.encode_idx, enc_in, enc_out)
120
+ states = enc_out[0].read((1, 207, 288), np.float32)
121
+
122
+ tokens = np.full((1, MAX_TOKENS), EOS_TOKEN, dtype=np.int32)
123
+ tokens[0, 0] = START_TOKEN
124
+ dec_in = self.model.create_input_buffers(self.decode_idx)
125
+ dec_out = self.model.create_output_buffers(self.decode_idx)
126
+ dec_in[0].write(states)
127
+ dec_in[2].write(self.mask)
128
+
129
+ decoded = []
130
+ for position in range(1, MAX_TOKENS):
131
+ dec_in[1].write(tokens)
132
+ self.model.run_by_index(self.decode_idx, dec_in, dec_out)
133
+ logits = dec_out[0].read((1, MAX_TOKENS, 32768), np.float32)
134
+ next_token = int(np.argmax(logits[0, position - 1]))
135
+ if next_token == EOS_TOKEN:
136
+ break
137
+ tokens[0, position] = next_token
138
+ decoded.append(next_token)
139
+ return self.tokenizer.decode(decoded).strip()
140
+
141
+ def transcribe(self, audio: np.ndarray) -> str:
142
+ """Transcribes audio of any length in consecutive 5 s windows."""
143
+ parts = [
144
+ self._transcribe_window(audio[i : i + WINDOW_SAMPLES])
145
+ for i in range(0, max(len(audio), 1), WINDOW_SAMPLES)
146
+ ]
147
+ return " ".join(p for p in parts if p)
148
+
149
+
150
+ def main():
151
+ parser = argparse.ArgumentParser()
152
+ parser.add_argument("--wav", required=True, help="Path to a wav file.")
153
+ parser.add_argument(
154
+ "--variant", default="f32", choices=["f32", "i8"], help="Model variant."
155
+ )
156
+ args = parser.parse_args()
157
+
158
+ model_path = hf_hub_download(
159
+ "litert-community/moonshine-tiny", f"moonshine_tiny_5s_{args.variant}.tflite"
160
+ )
161
+ tokenizer_path = hf_hub_download("UsefulSensors/moonshine-tiny", "tokenizer.json")
162
+
163
+ asr = MoonshineTiny(model_path, tokenizer_path)
164
+ audio = load_wav_16k_mono(args.wav)
165
+ print(asr.transcribe(audio))
166
+
167
+
168
+ if __name__ == "__main__":
169
+ main()
170
+ ```
171
+
172
+ **3. Run it** on a 16 kHz mono wav file:
173
+
174
+ ```bash
175
+ python transcribe.py --wav sample.wav
176
+ ```
177
+
178
+ ### Android sample app
179
+
180
+ For an on-device Android demo that runs Moonshine (and other ASR models) with
181
+ hardware acceleration, see the LiteRT
182
+ [speech recognition sample](https://github.com/google-ai-edge/litert-samples/tree/main/samples/litert/speech_recognition).
183
+
184
+ ## Performance
185
+
186
+ Measured on one 5 s window of continuous speech (11 output tokens), CPU
187
+ inference, median of 10 runs. The macOS and Raspberry Pi rows use the Python
188
+ Interpreter API as in the script above (XNNPack, 4 threads,
189
+ `ai-edge-litert` 2.1.6); the iPhone rows use the LiteRT CompiledModel C API
190
+ with the CPU accelerator at default threading:
191
+
192
+ | Device | Variant | Encode | Decode | Window total | RTF |
193
+ |---|---|---|---|---|---|
194
+ | iPhone 17 Pro | f32 | 10.9 ms | 70.2 ms | 81.2 ms | 0.016 |
195
+ | iPhone 17 Pro | i8 | 16.3 ms | 67.5 ms | 84.1 ms | 0.017 |
196
+ | Apple M4 Max (macOS) | f32 | 8.1 ms | 79.4 ms | 87.5 ms | 0.017 |
197
+ | Apple M4 Max (macOS) | i8 | 8.8 ms | 76.7 ms | 85.5 ms | 0.017 |
198
+ | Raspberry Pi 5 | f32 | 50.7 ms | 444.7 ms | 495.3 ms | 0.099 |
199
+ | Raspberry Pi 5 | i8 | 33.4 ms | 269.4 ms | 303.1 ms | 0.061 |
200
+
201
+ RTF = processing time / audio duration (lower is better; below 1.0 is faster
202
+ than real time). Decode dominates and scales with the number of emitted
203
+ tokens, so dense speech takes proportionally longer than sparse speech. The
204
+ i8 model runs about 1.6x faster than f32 on the Pi 5's Cortex-A76; on Apple
205
+ silicon (M4 Max, iPhone 17 Pro) the two are equally fast. The f32 greedy
206
+ decode is deterministic across platforms: the same window produces
207
+ bit-identical token sequences on all three devices.
208
+
209
+ ### Accuracy note
210
+
211
+ In a 12-clip spot check (LibriSpeech dev-clean samples plus two
212
+ public-domain clips), the f32 model transcribes clips of up to 5 s at
213
+ near-reference quality. The i8 variant currently shows significant
214
+ transcription degradation on the same clips, which isolates to its quantized
215
+ encoder (an f32 encoder with the i8 decoder matches full-f32 output almost
216
+ exactly). Until a recalibrated i8 encoder is published, the f32 model — or
217
+ the f32 encoder combined with the i8 decoder — is recommended where
218
+ transcription quality matters.
219
+
220
+ For the source model's quality, the
221
+ [Moonshine paper](https://arxiv.org/abs/2410.15608) reports that Moonshine
222
+ Tiny matches Whisper tiny.en word error rates across standard evaluation
223
+ datasets at about 5x less compute.
224
+
225
+ ## License and attribution
226
+
227
+ The original Moonshine Tiny model is released by Moonshine AI under the MIT
228
+ license; these converted artifacts inherit it. If you use this model, please
229
+ cite:
230
+
231
+ ```bibtex
232
+ @misc{jeffries2024moonshinespeechrecognitionlive,
233
+ title={Moonshine: Speech Recognition for Live Transcription and Voice Commands},
234
+ author={Nat Jeffries and Evan King and Manjunath Kudlur and Guy Nicholson and James Wang and Pete Warden},
235
+ year={2024},
236
+ eprint={2410.15608},
237
+ archivePrefix={arXiv},
238
+ primaryClass={cs.SD},
239
+ url={https://arxiv.org/abs/2410.15608},
240
+ }
241
+ ```