Instructions to use KlangAI/pianissimo-da-mlx-8bit with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- MLX
How to use KlangAI/pianissimo-da-mlx-8bit with MLX:
# Download the model from the Hub pip install huggingface_hub[hf_xet] hf download KlangAI/pianissimo-da-mlx-8bit --local-dir pianissimo-da-mlx-8bit
- Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- LM Studio
- Atomic Chat
Commit ·
a745fed
0
Parent(s):
Initial release
Browse files- .gitattributes +35 -0
- README.md +92 -0
- config.json +0 -0
- manifest.json +20 -0
- model.safetensors +3 -0
- phrase_boost.py +84 -0
- pianissimo_mlx.py +147 -0
.gitattributes
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
+
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
+
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
+
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
+
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
+
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
+
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
+
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
+
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
+
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
+
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
+
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
+
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
+
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
+
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
+
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
+
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
+
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
+
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
+
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
+
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
+
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
+
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
+
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
README.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
language:
|
| 3 |
+
- da
|
| 4 |
+
license: cc-by-4.0
|
| 5 |
+
pipeline_tag: automatic-speech-recognition
|
| 6 |
+
library_name: mlx
|
| 7 |
+
base_model: KlangAI/pianissimo-da
|
| 8 |
+
base_model_relation: quantized
|
| 9 |
+
tags:
|
| 10 |
+
- mlx
|
| 11 |
+
- speech
|
| 12 |
+
- audio
|
| 13 |
+
- fastconformer
|
| 14 |
+
- tdt
|
| 15 |
+
- danish
|
| 16 |
+
---
|
| 17 |
+
|
| 18 |
+
# Klang Pianissimo for Danish: MLX 8-bit
|
| 19 |
+
|
| 20 |
+
A version of [Klang Pianissimo for Danish](https://huggingface.co/KlangAI/pianissimo-da), our Danish speech recognition model, for Macs with Apple silicon (M1 or later). It runs on the Mac's GPU using [MLX](https://github.com/ml-explore/mlx) and [parakeet-mlx](https://github.com/senstella/parakeet-mlx), without PyTorch or NeMo.
|
| 21 |
+
|
| 22 |
+
This repository contains the 8-bit version, with almost the same accuracy as the full-size model at less than a third of the download size.
|
| 23 |
+
|
| 24 |
+
## Versions
|
| 25 |
+
|
| 26 |
+
| Repository | Weights | Download | FLEURS test | Speed | Memory, 5 min |
|
| 27 |
+
|---|---|---:|---:|---:|---:|
|
| 28 |
+
| [pianissimo-da](https://huggingface.co/KlangAI/pianissimo-da) | Original fp32 | 2.51 GB | **7.43** | | |
|
| 29 |
+
| [pianissimo-da-mlx-8bit](https://huggingface.co/KlangAI/pianissimo-da-mlx-8bit) | 8-bit | 759 MB | 7.49 | 151× | **2.2 GB** |
|
| 30 |
+
| [pianissimo-da-mlx-4bit](https://huggingface.co/KlangAI/pianissimo-da-mlx-4bit) | 4-bit | **528 MB** | 7.80 | 126× | **2.2 GB** |
|
| 31 |
+
| [pianissimo-da-mlx](https://huggingface.co/KlangAI/pianissimo-da-mlx) | bf16 | 1.25 GB | 7.48 | 140× | 2.7 GB |
|
| 32 |
+
| [pianissimo-da-mlx-fp32](https://huggingface.co/KlangAI/pianissimo-da-mlx-fp32) | fp32 | 2.51 GB | **7.43** | **173×** | 3.8 GB |
|
| 33 |
+
|
| 34 |
+
The FLEURS column shows word error rate (WER, %) on all 930 Danish test clips, scored with our evaluation normaliser; lower is better. Speed is audio duration divided by processing time (RTFx), measured on a 30-second clip.
|
| 35 |
+
Speed and memory were measured on the Swedish model, which has the same architecture, on an Apple M5 Pro GPU (24 GB). Memory is peak GPU memory use while transcribing a 5-minute recording.
|
| 36 |
+
|
| 37 |
+
For other computers, use the [ONNX versions](https://huggingface.co/KlangAI/pianissimo-da-onnx).
|
| 38 |
+
|
| 39 |
+
## Usage
|
| 40 |
+
|
| 41 |
+
```bash
|
| 42 |
+
pip install parakeet-mlx==0.5.2 huggingface_hub sentencepiece
|
| 43 |
+
brew install ffmpeg # to read audio files
|
| 44 |
+
```
|
| 45 |
+
|
| 46 |
+
```python
|
| 47 |
+
import sys
|
| 48 |
+
from huggingface_hub import snapshot_download
|
| 49 |
+
|
| 50 |
+
path = snapshot_download("KlangAI/pianissimo-da-mlx-8bit")
|
| 51 |
+
sys.path.append(path)
|
| 52 |
+
import pianissimo_mlx
|
| 53 |
+
|
| 54 |
+
model = pianissimo_mlx.load(path)
|
| 55 |
+
result = pianissimo_mlx.transcribe(model, "audio.wav")
|
| 56 |
+
print(result.text)
|
| 57 |
+
```
|
| 58 |
+
|
| 59 |
+
Recordings longer than 2 minutes are transcribed in 2-minute chunks with a 15-second overlap. Set `chunk_duration` to change the chunk length in seconds, or to `None` for a single pass. `transcribe` also accepts a 16 kHz mono NumPy array. Timestamps are available in `result.sentences` and `sentence.tokens`.
|
| 60 |
+
|
| 61 |
+
Use the `pianissimo_mlx.py` loader included in this repository. It supports the 8-bit and 4-bit weights and reproduces the audio preprocessing used during training. Calling parakeet-mlx directly does not support these quantized weights and uses slightly different preprocessing, adding about 0.2 percentage points of WER.
|
| 62 |
+
|
| 63 |
+
### Vocabulary
|
| 64 |
+
|
| 65 |
+
Our implementation of NeMo's phrase boosting supports custom vocabulary and matches NeMo's results on the original model. List names and terms to help the model spell them correctly.
|
| 66 |
+
|
| 67 |
+
```python
|
| 68 |
+
vocabulary = ["Klang AI", "Pianissimo", "Folketinget", "Aarhus Universitet"]
|
| 69 |
+
result = pianissimo_mlx.transcribe(model, "audio.wav", boost=vocabulary)
|
| 70 |
+
```
|
| 71 |
+
|
| 72 |
+
See the [main model card](https://huggingface.co/KlangAI/pianissimo-da#vocabulary) for vocabulary boosting with NeMo.
|
| 73 |
+
|
| 74 |
+
## How they were made
|
| 75 |
+
|
| 76 |
+
All versions are converted from the released checkpoint and retain its local attention (256 frames on each side). The 8-bit and 4-bit versions use MLX's built-in quantization for the encoder's linear layers, with groups of 64 weights for 8-bit and 32 for 4-bit. The remaining weights in those two versions are stored in bfloat16.
|
| 77 |
+
|
| 78 |
+
FLEURS WER was measured with this code on Linux. In a separate Mac check, this version matched the normalised original fp32 reference on 187 of 200 FLEURS clips. See the [main model card](https://huggingface.co/KlangAI/pianissimo-da) for comparisons with other models.
|
| 79 |
+
|
| 80 |
+
## License and citation
|
| 81 |
+
|
| 82 |
+
Released under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/), like the original model.
|
| 83 |
+
|
| 84 |
+
```bibtex
|
| 85 |
+
@misc{klang2026pianissimoda,
|
| 86 |
+
title = {Klang Pianissimo for Danish},
|
| 87 |
+
author = {{Klang}},
|
| 88 |
+
year = {2026},
|
| 89 |
+
howpublished = {Hugging Face model repository},
|
| 90 |
+
url = {https://huggingface.co/KlangAI/pianissimo-da}
|
| 91 |
+
}
|
| 92 |
+
```
|
config.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
manifest.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"artifact": "pianissimo-da-mlx-8bit",
|
| 3 |
+
"checkpoint_sha256": "ef346b91fc125926ed8a5a69091b591bf59d0bc32e2d5d197f112a2a0b12d41b",
|
| 4 |
+
"mlx": "0.32.2",
|
| 5 |
+
"quantization": {
|
| 6 |
+
"group_size": 64,
|
| 7 |
+
"bits": 8,
|
| 8 |
+
"modules": "nn.Linear in encoder.layers"
|
| 9 |
+
},
|
| 10 |
+
"files": {
|
| 11 |
+
"config.json": {
|
| 12 |
+
"bytes": 200943,
|
| 13 |
+
"sha256": "93895461c005ccae88cffbec31c7ad05b9f44635cf3958670123c53a370013c8"
|
| 14 |
+
},
|
| 15 |
+
"model.safetensors": {
|
| 16 |
+
"bytes": 758783094,
|
| 17 |
+
"sha256": "61b8fec4b0a9d134812eb59f4482ffcca9ec03877b97df65189236513072b4f1"
|
| 18 |
+
}
|
| 19 |
+
}
|
| 20 |
+
}
|
model.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:61b8fec4b0a9d134812eb59f4482ffcca9ec03877b97df65189236513072b4f1
|
| 3 |
+
size 758783094
|
phrase_boost.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Phrase boosting for Klang Pianissimo. A numpy port of NeMo's GPU-PB boosting tree
|
| 2 |
+
(ContextGraph and GPUBoostingTreeModel, NeMo 2.7, Apache 2.0)."""
|
| 3 |
+
import math
|
| 4 |
+
from collections import deque
|
| 5 |
+
|
| 6 |
+
import numpy as np
|
| 7 |
+
import sentencepiece as spm
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class _Node:
|
| 11 |
+
__slots__ = ("id", "children", "token_score", "node_score", "is_end", "fail")
|
| 12 |
+
|
| 13 |
+
def __init__(self, node_id, token_score, node_score, is_end):
|
| 14 |
+
self.id = node_id
|
| 15 |
+
self.children = {}
|
| 16 |
+
self.token_score = token_score
|
| 17 |
+
self.node_score = node_score
|
| 18 |
+
self.is_end = is_end
|
| 19 |
+
self.fail = None
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class PhraseBoost:
|
| 23 |
+
def __init__(self, phrases, tokenizer_path, vocab_size, context_score=1.0, depth_scaling=2.0):
|
| 24 |
+
sp = spm.SentencePieceProcessor(model_file=str(tokenizer_path))
|
| 25 |
+
self.vocab_size = vocab_size
|
| 26 |
+
self.root = _Node(0, 0.0, 0.0, False)
|
| 27 |
+
self.root.fail = self.root
|
| 28 |
+
self.nodes = [self.root]
|
| 29 |
+
for phrase in dict.fromkeys(phrases):
|
| 30 |
+
self._add(sp.encode(phrase), context_score, depth_scaling)
|
| 31 |
+
self._fill_fail()
|
| 32 |
+
self._cache = {}
|
| 33 |
+
|
| 34 |
+
def _add(self, tokens, context_score, depth_scaling):
|
| 35 |
+
node = self.root
|
| 36 |
+
for i, token in enumerate(tokens):
|
| 37 |
+
last = i == len(tokens) - 1
|
| 38 |
+
child = node.children.get(token)
|
| 39 |
+
if child is None:
|
| 40 |
+
token_score = context_score * depth_scaling + math.log(i + 1) if i > 0 else context_score
|
| 41 |
+
child = _Node(len(self.nodes), token_score, node.node_score + token_score, last)
|
| 42 |
+
node.children[token] = child
|
| 43 |
+
self.nodes.append(child)
|
| 44 |
+
else:
|
| 45 |
+
child.token_score = max(context_score, child.token_score)
|
| 46 |
+
child.node_score = node.node_score + child.token_score
|
| 47 |
+
child.is_end = last or child.is_end
|
| 48 |
+
node = child
|
| 49 |
+
|
| 50 |
+
def _fill_fail(self):
|
| 51 |
+
queue = deque()
|
| 52 |
+
for child in self.root.children.values():
|
| 53 |
+
child.fail = self.root
|
| 54 |
+
queue.append(child)
|
| 55 |
+
while queue:
|
| 56 |
+
node = queue.popleft()
|
| 57 |
+
for token, child in node.children.items():
|
| 58 |
+
fail = node.fail
|
| 59 |
+
while token not in fail.children and fail is not self.root:
|
| 60 |
+
fail = fail.fail
|
| 61 |
+
child.fail = fail.children.get(token, self.root)
|
| 62 |
+
queue.append(child)
|
| 63 |
+
|
| 64 |
+
def scores(self, state):
|
| 65 |
+
"""Bonus and next state for every token from `state`."""
|
| 66 |
+
if state not in self._cache:
|
| 67 |
+
bonus = np.zeros(self.vocab_size, dtype=np.float32)
|
| 68 |
+
nxt = np.full(self.vocab_size, -1, dtype=np.int64)
|
| 69 |
+
node, taken_back = self.nodes[state], 0.0
|
| 70 |
+
while True:
|
| 71 |
+
for token, child in node.children.items():
|
| 72 |
+
if nxt[token] < 0:
|
| 73 |
+
bonus[token] = taken_back + child.token_score
|
| 74 |
+
nxt[token] = child.id
|
| 75 |
+
if node is self.root:
|
| 76 |
+
unset = nxt < 0
|
| 77 |
+
bonus[unset] = taken_back
|
| 78 |
+
nxt[unset] = 0
|
| 79 |
+
break
|
| 80 |
+
if not node.is_end:
|
| 81 |
+
taken_back += node.fail.node_score - node.node_score
|
| 82 |
+
node = node.fail
|
| 83 |
+
self._cache[state] = (bonus, nxt)
|
| 84 |
+
return self._cache[state]
|
pianissimo_mlx.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Klang Pianissimo on Apple silicon with parakeet-mlx: loads the quantized versions, computes NeMo's
|
| 2 |
+
audio features, splits long audio into chunks and supports phrase boosting.
|
| 3 |
+
|
| 4 |
+
model = pianissimo_mlx.load("KlangAI/pianissimo-da-mlx-8bit")
|
| 5 |
+
print(pianissimo_mlx.transcribe(model, "audio.wav", boost=["Klang AI", "Pianissimo", "RixVox", "Common Voice"]).text)
|
| 6 |
+
"""
|
| 7 |
+
import json
|
| 8 |
+
import os
|
| 9 |
+
import shutil
|
| 10 |
+
import subprocess
|
| 11 |
+
|
| 12 |
+
import mlx.core as mx
|
| 13 |
+
import mlx.nn as nn
|
| 14 |
+
import numpy as np
|
| 15 |
+
from parakeet_mlx import tokenizer
|
| 16 |
+
from parakeet_mlx.alignment import (AlignedToken, merge_longest_common_subsequence, merge_longest_contiguous,
|
| 17 |
+
sentences_to_result, tokens_to_sentences)
|
| 18 |
+
from parakeet_mlx.utils import from_config
|
| 19 |
+
|
| 20 |
+
from phrase_boost import PhraseBoost
|
| 21 |
+
|
| 22 |
+
SAMPLE_RATE = 16000
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def load(repo_or_path):
|
| 26 |
+
"""A Hugging Face repo id or a local directory with config.json and model.safetensors."""
|
| 27 |
+
path = repo_or_path
|
| 28 |
+
if not os.path.isdir(path):
|
| 29 |
+
from huggingface_hub import snapshot_download
|
| 30 |
+
path = snapshot_download(repo_or_path, allow_patterns=["config.json", "model.safetensors", "tokenizer.model"])
|
| 31 |
+
cfg = json.load(open(os.path.join(path, "config.json")))
|
| 32 |
+
model = from_config(cfg)
|
| 33 |
+
q = cfg.get("quantization")
|
| 34 |
+
if q:
|
| 35 |
+
nn.quantize(model, group_size=q["group_size"], bits=q["bits"],
|
| 36 |
+
class_predicate=lambda p, m: isinstance(m, nn.Linear) and p.startswith("encoder.layers."))
|
| 37 |
+
model.load_weights(os.path.join(path, "model.safetensors"))
|
| 38 |
+
model.tokenizer_path = os.path.join(path, "tokenizer.model")
|
| 39 |
+
return model
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def read_audio(path):
|
| 43 |
+
"""Any format ffmpeg reads, as 16 kHz mono float32."""
|
| 44 |
+
if shutil.which("ffmpeg") is None:
|
| 45 |
+
raise RuntimeError("reading audio files needs ffmpeg (brew install ffmpeg), or pass a 16 kHz numpy array")
|
| 46 |
+
out = subprocess.run(["ffmpeg", "-nostdin", "-v", "error", "-i", str(path), "-f", "s16le", "-ac", "1",
|
| 47 |
+
"-ar", str(SAMPLE_RATE), "-"], capture_output=True, check=True).stdout
|
| 48 |
+
return np.frombuffer(out, np.int16).astype(np.float32) / 32768.0
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def features(audio, args):
|
| 52 |
+
"""NeMo's log-mel featurizer, ported to MLX."""
|
| 53 |
+
n_fft, win = args.n_fft, int(args.window_size * args.sample_rate)
|
| 54 |
+
hop = int(args.window_stride * args.sample_rate)
|
| 55 |
+
x = mx.concatenate([audio[:1], audio[1:] - 0.97 * audio[:-1]])
|
| 56 |
+
x = mx.pad(x, [(n_fft // 2, n_fft // 2)])
|
| 57 |
+
frames = mx.as_strided(x, shape=((x.shape[0] - n_fft) // hop + 1, n_fft), strides=(hop, 1))
|
| 58 |
+
window = mx.array(np.pad(np.hanning(win), (n_fft - win) // 2).astype(np.float32))
|
| 59 |
+
power = mx.abs(mx.fft.rfft(frames * window)) ** 2
|
| 60 |
+
logmel = mx.log(mx.matmul(power, args._filterbanks.T.astype(mx.float32)) + 2 ** -24)
|
| 61 |
+
valid = audio.shape[0] // hop
|
| 62 |
+
logmel = logmel[:valid]
|
| 63 |
+
mean = logmel.mean(axis=0, keepdims=True)
|
| 64 |
+
std = mx.sqrt(((logmel - mean) ** 2).sum(axis=0, keepdims=True) / (valid - 1))
|
| 65 |
+
return mx.expand_dims((logmel - mean) / (std + 1e-5), 0)
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def transcribe(model, audio, chunk_duration=120.0, overlap_duration=15.0, boost=None, boost_alpha=1.0):
|
| 69 |
+
"""audio is a path or a 16 kHz mono numpy array. Audio longer than chunk_duration seconds is
|
| 70 |
+
transcribed in overlapping chunks (None for one pass). boost lists phrases to spell as written."""
|
| 71 |
+
if isinstance(audio, (str, os.PathLike)):
|
| 72 |
+
audio = read_audio(audio)
|
| 73 |
+
audio = np.asarray(audio, dtype=np.float32)
|
| 74 |
+
tree = PhraseBoost(boost, model.tokenizer_path, vocab_size=len(model.vocabulary)) if boost else None
|
| 75 |
+
if chunk_duration is None or len(audio) <= chunk_duration * SAMPLE_RATE:
|
| 76 |
+
return _transcribe_piece(model, audio, tree, boost_alpha)
|
| 77 |
+
|
| 78 |
+
step = int((chunk_duration - overlap_duration) * SAMPLE_RATE)
|
| 79 |
+
size = int(chunk_duration * SAMPLE_RATE)
|
| 80 |
+
tokens = []
|
| 81 |
+
for start in range(0, len(audio), step):
|
| 82 |
+
result = _transcribe_piece(model, audio[start:start + size], tree, boost_alpha)
|
| 83 |
+
offset = start / SAMPLE_RATE
|
| 84 |
+
for sentence in result.sentences:
|
| 85 |
+
for token in sentence.tokens:
|
| 86 |
+
token.start += offset
|
| 87 |
+
token.end = token.start + token.duration
|
| 88 |
+
if not tokens:
|
| 89 |
+
tokens = result.tokens
|
| 90 |
+
else:
|
| 91 |
+
try:
|
| 92 |
+
tokens = merge_longest_contiguous(tokens, result.tokens, overlap_duration=overlap_duration)
|
| 93 |
+
except RuntimeError:
|
| 94 |
+
tokens = merge_longest_common_subsequence(tokens, result.tokens, overlap_duration=overlap_duration)
|
| 95 |
+
mx.clear_cache()
|
| 96 |
+
if start + size >= len(audio):
|
| 97 |
+
break
|
| 98 |
+
return sentences_to_result(tokens_to_sentences(tokens))
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def _transcribe_piece(model, audio, tree, alpha):
|
| 102 |
+
mel = features(mx.array(audio), model.preprocessor_config)
|
| 103 |
+
if tree is None:
|
| 104 |
+
return model.generate(mel)[0]
|
| 105 |
+
encoded, lengths = model.encoder(mel)
|
| 106 |
+
mx.eval(encoded, lengths)
|
| 107 |
+
return sentences_to_result(tokens_to_sentences(_boosted_greedy(model, encoded, int(lengths[0]), tree, alpha)))
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def _boosted_greedy(model, encoded, length, tree, alpha):
|
| 111 |
+
"""parakeet-mlx's greedy TDT decoding with the phrase bonus added."""
|
| 112 |
+
blank = len(model.vocabulary)
|
| 113 |
+
hypothesis, last_token, hidden_state = [], None, None
|
| 114 |
+
boost_state, step, new_symbols = 0, 0, 0
|
| 115 |
+
while step < length:
|
| 116 |
+
decoder_out, (hidden, cell) = model.decoder(
|
| 117 |
+
mx.array([[last_token]]) if last_token is not None else None, hidden_state)
|
| 118 |
+
decoder_out = decoder_out.astype(encoded.dtype)
|
| 119 |
+
decoder_hidden = (hidden.astype(encoded.dtype), cell.astype(encoded.dtype))
|
| 120 |
+
joint_out = model.joint(encoded[:, step:step + 1], decoder_out)
|
| 121 |
+
|
| 122 |
+
token_logits = joint_out[0, 0, :, :blank + 1]
|
| 123 |
+
pred_token = int(mx.argmax(token_logits))
|
| 124 |
+
token_probs = mx.softmax(token_logits, axis=-1)
|
| 125 |
+
entropy = -mx.sum(token_probs * mx.log(token_probs + 1e-10), axis=-1)
|
| 126 |
+
confidence = float(1.0 - entropy / mx.log(mx.array(blank + 1, dtype=token_probs.dtype)))
|
| 127 |
+
decision = int(mx.argmax(joint_out[0, 0, :, blank + 1:]))
|
| 128 |
+
|
| 129 |
+
if pred_token != blank:
|
| 130 |
+
bonus, next_states = tree.scores(boost_state)
|
| 131 |
+
logits = np.array(token_logits[0, :blank].astype(mx.float32))
|
| 132 |
+
pred_token = int(np.argmax(logits + alpha * bonus))
|
| 133 |
+
boost_state = int(next_states[pred_token])
|
| 134 |
+
hypothesis.append(AlignedToken(
|
| 135 |
+
pred_token, start=step * model.time_ratio, duration=model.durations[decision] * model.time_ratio,
|
| 136 |
+
confidence=confidence, text=tokenizer.decode([pred_token], model.vocabulary)))
|
| 137 |
+
last_token = pred_token
|
| 138 |
+
hidden_state = decoder_hidden
|
| 139 |
+
|
| 140 |
+
step += model.durations[decision]
|
| 141 |
+
new_symbols += 1
|
| 142 |
+
if model.durations[decision] != 0:
|
| 143 |
+
new_symbols = 0
|
| 144 |
+
elif model.max_symbols is not None and model.max_symbols <= new_symbols:
|
| 145 |
+
step += 1
|
| 146 |
+
new_symbols = 0
|
| 147 |
+
return hypothesis
|