funasr-mlx
Native MLX inference ports of the FunASR model zoo for Apple Silicon — memory-bounded, numerically verified against the official references, with no PyTorch/runtime dependency at inference time.
All 14 checkpoints covering 11 architectures from the FunASR README model zoo are ported. Every port is validated layer-by-layer against its official implementation (fp32, shared-input parity) and end-to-end on real audio before it is marked done.
Coverage & Verification
| Model | Family | Layer parity (fp32) | Real-audio e2e |
|---|---|---|---|
| SenseVoiceSmall | sensevoice |
source-exact diffs | ✅ |
| Paraformer (offline) | paraformer |
rel 1e-6 (GEMM noise) | ✅ CER ~0 |
| Paraformer streaming | paraformer |
4.0e-7 tail path | ⚠️ CER 0.156* |
| Whisper tiny | whisper |
canonical full-forward | ✅ |
| Whisper large-v3 | whisper |
enc 1.5e-4 / dec 2.3e-5 | ✅ RTF 0.058 |
| Whisper large-v3-turbo | whisper |
enc 5.3e-4 / dec 2.6e-4 | ✅ RTF 0.061 |
| emotion2vec+large | emotion2vec |
fairseq-exact CNN/pos | ✅ |
| ct-punc | ct-punc |
conv-tap exact | ✅ |
| fsmn-vad | fsmn-vad |
conv/cache exact | ✅ |
| CAM++ | campplus |
source-exact | ✅ |
| Fun-ASR-Nano | llm | audio 4.6e-3 / LLM 3.0e-6 | ✅ 8/8 |
| Qwen3-ASR-0.6B | llm | audio 1.2e-5 / dec 1.4e-4 | ✅ 6/6 |
| GLM-ASR-Nano-2512 | llm | tower 2.0e-5 / llama 3.3e-6 | ✅ 7/7 |
| MOSS-Transcribe-Diarize | llm | enc 1.3e-3 / dec 4.7e-5 | ✅ 8/8 +timestamps+speakers |
* Streaming Paraformer: single-layer diffs are 1e-6, but the chunked streaming state machine amplifies fp32 noise until CIF fire positions drift (MLX has no fp64). Use streaming for partial results and the offline model for final transcripts.
The four LLM-based models (Fun-ASR-Nano, Qwen3-ASR, GLM-ASR-Nano,
MOSS-Transcribe-Diarize) are ported and numerically verified
(src/funasr_mlx/models/ + models/llm/), with e2e validated through
bf16 scripts; their pipeline-factory registration is pending. The seven
speech families above run through the CLI today.
Local Use
Install on an Apple Silicon Mac with Python 3.11+:
python3 -m pip install -e .
funasr-mlx --help
Inspect a local checkpoint or extract bounded features:
funasr-mlx inspect /path/to/model.safetensors
funasr-mlx features recording.wav --cmvn none
Inference requires a local converted checkpoint directory and a matching native JSON or YAML configuration:
funasr-mlx infer /path/to/checkpoint-dir --model sensevoice --audio recording.wav
funasr-mlx infer /path/to/checkpoint-dir --model ct-punc --text "hello world" --tokens /path/to/tokens.txt
W4A16 quantization
4-bit weights with 16-bit activations for the LLM-ASR decoders, using MLX affine group quantization (group size 64). At batch size 1 the decode step streams the weights once per step, so reading 4x fewer bytes is the largest remaining lever; measured on an M4 it cuts peak inference memory 41-59% and decode time up to 2.7x across the five families.
Convert a native checkpoint, then load it exactly as an unquantized one:
python3 tools/convert_llm_checkpoints.py --family qwen3-asr-0.6b \
--source /path/to/qwen3-asr-0.6b --destination /path/to/qwen3-asr-0.6b-w4a16 \
--native-source --quantize tower
--quantize picks how much is quantized, and the choice is not cosmetic:
| scope | reaches | buys |
|---|---|---|
decoder |
decoder Linear layers | decode time |
tower |
+ the audio tower | peak memory; the tower runs once per utterance, so no decode cost |
all |
+ the tied token embedding | removes the per-step LM-head read, but only some families survive it |
The token embedding is the tied LM head, so quantizing it injects 4-bit error
directly into every step's logits. It is safe for fun-asr-nano (tokens fully
identical) and destroys the transcript for qwen3-asr-0.6b and glm-asr-nano,
so all is opt-in per family rather than a default. lm_head, position tables,
convolutions and norms are never quantized. Loading is strict: the config
records quantized_tensors and quantization_skipped, and a key that should
have been quantized but was not is an error, not a silent fallback.
Per-family scope, checkpoint sizes, measured speedups and peak-memory drops are
in .tmp/t9/RESULTS.md; the scheme and its failure modes are in
.tmp/t9/LEDGER.md.
Downloading published checkpoints
No hub dependencies (stdlib HTTPS only; HF and MS mirrors with per-file fallback; ~5-6 MB/s ceiling is the hosts' per-IP throttle, not client):
funasr-mlx download qwen3-asr-0.6b-mlx --variant w4a16 --dest ./models
# classic models omit --variant:
funasr-mlx download sensevoice-mlx-fp16 --dest ./models
Then transcribe directly from the download:
funasr-mlx transcribe ./models/qwen3-asr-0.6b-mlx/w4a16 --audio recording.wav
LLM-ASR transcription
Three interfaces share one resident-worker protocol (KV cache reset-reuse between requests; RTF 0.079 on the 0.6B w4, M4 Air):
CLI (single file):
funasr-mlx transcribe /path/to/qwen3-asr-0.6b-w4a16-awq-w8tower-g32 --audio recording.wav
# {"text": "...", "token_ids": [...], "family": "qwen3-asr", "wall_ms": 650.8}
Python API (loads in-process directly):
from funasr_mlx.api import transcribe, download_model
path = download_model("qwen3-asr-0.6b-mlx", variant="w4a16", dest="./models")
result = transcribe("recording.wav", model=path / "w4a16")
print(result["text"])
HTTP server (OpenAI-compatible endpoint; raw body or multipart form):
funasr-mlx serve /path/to/checkpoint --port 8000
curl -X POST http://127.0.0.1:8000/v1/audio/transcriptions -F "file=@recording.wav"
Supported families: qwen3-asr (0.6B/1.7B), glm-asr-nano,
moss-transcribe-diarize, fun-asr-nano (family inferred from the
checkpoint directory name when omitted).
Pseudo-streaming (all transcription families)
funasr_mlx.pseudo_streaming.PseudoStream gives every transcription model
a streaming-style API over a growing audio buffer: each push() re-runs the
model on buffer[0:t], close() returns output bit-identical to offline
(measured: 94.6% of displayed characters survive to final; revisions are
confined to the rolling tail, .tmp/opt/LOG.md).
- LLM families (
qwen3-asr,glm-asr-nano,moss,fun-asr-nano):TranscribeWorker.pseudo_stream(); tower+prefill recompute per push (~150-800ms per 1s chunk on the 0.6B w4). - Classic families (
sensevoice,paraformer): single-pass per push;classic_transcriber(model, config, family=..., tokens=..., cmvn=...)(cmvn REQUIRED for sensevoice -- withoutam.mvnthe encoder collapses to all blanks).
from funasr_mlx.api import TranscribeWorker
worker = TranscribeWorker("models/qwen3-asr-0.6b-mlx/w4a16")
stream = worker.pseudo_stream(chunk_seconds=1.0)
for chunk in mic: # numpy float32 @16k, any sizes
ev = stream.push(chunk)
if ev: render(ev.text) # stable prefix + rolling tail
final = stream.close() # == offline transcription
Development
Optional test, reference and convert dependency groups. Source
differential tests use a sibling FunASR checkout; unavailable references
skip. Verification:
PYTHONPATH=src PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 python3 -m funasr_mlx.runtime \
--root . --report .tmp/check.json -- \
python3 -m pytest -q --tb=short --basetemp=.tmp/pytest-check
Tests pass in the suite. Real-checkpoint accuracy, quantized
variants, and latency/RTF figures are documented in PROGRESS.md; the W4A16
per-family numbers are in .tmp/t9/RESULTS.md.
Attribution
Qwen3/Llama decoder layouts and the KV cache are vendored (trimmed) from mlx-lm (Apache 2.0, Copyright © 2023-2024 Apple Inc.); source and license notes are in the module docstrings. Model architectures follow their official references (FunASR, OpenAI Whisper, Qwen, GLM, OpenMOSS).
Status
Experimental: numerically verified ports, not a production service. No fastest / lowest-memory / production-quality claim is made.
Metadata
Release files for funasr-mlx 0.2.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| funasr_mlx-0.2.2.tar.gz | 517.9 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| funasr_mlx-0.2.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 652.7 kB
Release files / funasr_mlx-0.2.2.tar.gz
| Download URL | funasr_mlx-0.2.2.tar.gz |
|---|---|
| Size | 517.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ee15260fac91edd09bd0a832aafc47453fae30cd5297955d7848f6441c6e11f0
|
|
BLAKE2b-256 checksum How to use checksums |
ae32b1cafbb54e497ad1e83d06a83376b9c3e755e43d45725e2696442f64e9d1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.13.4 {"installer":{"name":"uv","version":"0.13.4","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / funasr_mlx-0.2.2-py3-none-any.whl
| Download URL | funasr_mlx-0.2.2-py3-none-any.whl |
|---|---|
| Size | 134.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
5744d7eec2d06714728414a387ceb57663a99758fcee639389b31e31efb95a8c
|
|
BLAKE2b-256 checksum How to use checksums |
139e14d558a1a85bdd71df17486f83710c8230cff7b97ce3ac86c0e938e8fae2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.13.4 {"installer":{"name":"uv","version":"0.13.4","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Security for funasr-mlx 0.2.2
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.