Instructions to use litert-community/moonshine-tiny with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- LiteRT
How to use litert-community/moonshine-tiny with LiteRT:
# No code snippets available yet for this library. # To use this model, check the repository files and the library's documentation. # Want to help? PRs adding snippets are welcome at: # https://github.com/huggingface/huggingface.js
- Notebooks
- Google Colab
- Kaggle
Add model card: usage script + measured performance (iPhone 17 Pro / M4 Max / Raspberry Pi 5)
#1
by mlboydaisuke - opened
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 |
+
```
|