You need to agree to share your Hugging Face information to access this model

This repository is publicly accessible, but you have to accept the conditions to access its files and content.

This model is released under the Quran-Lab No-Profit License 1.2 (NPL-1.2). By requesting access you agree: (1) never to charge for the model, for access to it, or for any feature it powers, as defined in the LICENSE, noting that what you earn from your own teaching, services or labour is your own, (2) not to present its output as an authoritative religious ruling on anyone's recitation, and (3) to state clearly, in any application built on it, that automatic tajweed feedback can be wrong and does not replace a qualified teacher.

Log in or Sign Up to review the conditions and access this model content.

zipformer_p-arabic-v3

A 65.5M-parameter streaming phoneme recogniser for Qur'an recitation (Hafs 'an 'Asim), built on Zipformer2 with a CTC head. It transcribes recitation directly into a 251-symbol Qur'anic phonetic alphabet that encodes tajweed-relevant distinctions: madd length, gemination, ghunna, ikhfaa, qalqalah, and the emphatic consonants.

Design principle: no language model, anywhere. No transducer, no LM rescoring, no text prior. An internal or external LM silently corrects a reciter's mistakes, which is precisely what a recitation-assessment system must never do. Accuracy comes from data and the encoder alone.

Latest: v3.2 (2026-10-31), the corrected export of v3.1: same weights, same interface, same tokens.txt, and it computes the network v3.1 was actually trained as. See the v3.2 section below. On Apple devices it ships as a CoreML package that runs entirely on the Neural Engine (zipformer_p_arabic_v3.2.float16.mlpackage) and as the signed model folder the Quran Lab Swift SDK loads (sdk/v3.2).

Results

Phoneme Error Rate on the open Quranic ASR Benchmark (v1.1 references), against the previous public release of this line:

held-out set Quran Model zipformer_p-arabic-v2
studio reciters, 3 benchmark reciters* 1.43% 5.19%
real phone recordings 3.65% 7.92%
unseen professional reciter 9.10% 11.91%

Measured on the shipped checkpoint (SHA-verified) with the public per-eval manifest; an earlier revision of this card carried 4.92% for the unseen reciter, which does not reproduce on the shipped weights and has been corrected.

* The three studio benchmark reciters were intended to be fully absent from training; a post-release audit found 24 stray clips of them (0.001% of the 1.85M-row finetune manifest, none of the 600 benchmark clips) introduced by the concatenation builder. Reported as-is for transparency.

The emphatic-consonant gap (Ψ΅ ΨΆ Ψ· ΨΈ error rate minus their plain counterparts), a known weakness of earlier models, is statistically indistinguishable from zero on all three sets.

On obadx/qdat_bench (159 everyday reciters, one ayah, human-annotated tajweed attributes), madd-length measurement by attribute (RMSE in harakat, lower is better):

attribute Quran Model muaalem-model-v3_2
qalo_alif 0.210 0.449
qalo_waw 0.514 0.456
allam_alif 0.363 n/a
separate madd (munfasil) 0.887 0.687
madd aared 1.458 n/a

Honest limitations

These are measured, not hypothetical. Read them before building on the model.

  1. It is trained almost entirely on correct recitation with canonical labels. When a reciter deviates from the rule, the model tends to transcribe the rule rather than the deviation. On qdat_bench it reported full ikhfaa for 81 of 94 clips where human annotators heard a plain noon. Do not use it, as shipped, as a standalone judge of whether ikhfaa or similar quality-contrasts were performed.
  2. Free-choice madd lengths (munfasil, aared) are unreliable in the token stream because the training labels fix them at 4 harakat while reciters legitimately choose 2, 4 or 6. Duration grading should be done from forced-alignment timing, not from the emitted symbol run-length.
  3. Children recite into its weakest region: under-12 reciters score 2 to 3 times worse than adults on qdat_bench.
  4. Chunk size matters and depends on domain. On in-domain recitation, streaming chunks (16 or 24 frames) match or beat full context. On out-of-domain audio, full context is about 2 PER points better. The model was trained only at chunk sizes 8, 16 and 24.

v3.1 (madd fine-tune)

zipformer_p_arabic_v3.1.* is a fine-tune of v3 that retunes madd (elongation) length behaviour, targeting limitation 2 above. It is the same architecture, the same 99-in/99-out streaming interface and the same tokens.txt, so it is a drop-in replacement at the call site in every format, including the CoreML packages (whose state-packing manifests are byte-identical to v3's). Every weight differs from v3, so evaluate it on your own material before switching; duration grading from forced-alignment timing remains more reliable than run-length counting either way.

v3.2 (corrected v3.1 export, 2026-10-31)

The released zipformer_p_arabic_v3.1(.int8).onnx files run the SwooshL activation at offset 4, the stock icefall value, but v3.1 was trained with offset 1, so those files compute a slightly different network from the one that was trained. v3.2 (zipformer_p_arabic_v3.2.onnx and zipformer_p_arabic_v3.2.int8.onnx) folds the trained activation exactly into the standard graph (SwooshL_1(x) = SwooshL_4(x + 3) + 0.24 at all 49 sites). It is a drop-in replacement for v3.1: the same 99 inputs and outputs, the same metadata and the same tokens.txt. Its streaming output matches the trained network (143 of 143 clips greedy-identical).

Same weights, two exports, measured on our internal development sets (int8, chunk 24, left context 256, streaming with a 1.05 s flush). These sets are not the public benchmark; the two columns are a like-for-like comparison.

v3.2 v3.1 (released files)
planted vowel mistakes reported 39.5% 24.5%
planted qalqalah deletions reported 87.4% 68.8%
learner recitation PER 15.1 24.5
correct recitation PER 2.65 2.58

We recommend v3.2. Its int8 file was re-quantized on 2026-10-31 with v3.1's recipe (symmetric int8 weights on the matrix multiplies, convolutions in fp32). It matches fp32 on our faithfulness sets and on recitation PER; the first v3.2 int8 upload, used for the table above, reported about 5 fewer planted vowel mistakes than fp32.

v3.2 at shorter chunks (2026-11-01)

The standard files decode 24 output frames per call (0.48 s of audio). For apps that need words to appear sooner, zipformer_p_arabic_v3.2_chunk16(.int8).onnx decode 16 frames per call (0.32 s) and zipformer_p_arabic_v3.2_chunk8(.int8).onnx decode 8 (0.16 s). They are the same v3.2 network (v3.1 weights, trained activation), rebuilt at the shorter chunk; rebuilding the 24-frame model the same way reproduces zipformer_p_arabic_v3.2.onnx (greedy-identical on 75 of 75 clips). Left context stays 256 frames, the cache states, metadata keys and tokens.txt are unchanged, and sherpa-onnx loads them with the same from_zipformer2_ctc call. The int8 files use the same recipe as zipformer_p_arabic_v3.2.int8.onnx.

chunk audio per call decode_chunk_len / T token edits vs the 24-frame fp32 model (fp32 / int8) CPU per call, int8 (M2 Pro, 1 thread)
24 (standard files) 0.48 s 48 / 61 reference 21.4 ms
16 0.32 s 32 / 45 0.45% / 0.49% 11.3 ms
8 0.16 s 16 / 29 0.60% / 0.65% 9.2 ms

Start with 16 and only go to 8 if you still need less delay.

Files

file purpose
zipformer_p_arabic_v3.pt PyTorch weights (average of the last 3 training epochs)
zipformer_p_arabic_v3.onnx / zipformer_p_arabic_v3.int8.onnx cache-aware streaming CTC export, sherpa-onnx compatible
zipformer_p_arabic_v3.float8.mlpackage CoreML, 8-bit weights + fp16 compute, Apple Neural Engine, iOS 18+ (multifunction: front and back). Recommended for iOS.
zipformer_p_arabic_v3.float16.mlpackage CoreML fp16, same graph and interface as float8
zipformer_p_arabic_v3.1.pt v3.1, the madd fine-tune (see below): PyTorch weights
zipformer_p_arabic_v3.1.onnx / zipformer_p_arabic_v3.1.int8.onnx v3.1 streaming CTC export, same interface as v3
zipformer_p_arabic_v3.2.onnx / zipformer_p_arabic_v3.2.int8.onnx v3.2, the corrected v3.1 export (2026-10-31, see above): same weights, interface and tokens.txt. Recommended
zipformer_p_arabic_v3.2_chunk16.onnx / zipformer_p_arabic_v3.2_chunk16.int8.onnx v3.2 at a 16-frame chunk (0.32 s per call, lower latency; see v3.2 at shorter chunks): same weights, states and tokens.txt
zipformer_p_arabic_v3.2_chunk8.onnx / zipformer_p_arabic_v3.2_chunk8.int8.onnx v3.2 at an 8-frame chunk (0.16 s per call): same weights, states and tokens.txt
zipformer_p_arabic_v3.2.float16.mlpackage v3.2 CoreML, fp16, every op on the Apple Neural Engine, iOS 18+ (functions front, back, front_b2, back_b2; see Usage, CoreML). Recommended for Apple devices
keeps_packed.bin the host's keep table for the v3.2 CoreML package
sdk/v3.2/ the signed model folder the Quran Lab Swift SDK loads: the compiled v3.2 package, engine.zfs (the exact CPU engine's weights, rebuilt from the package's weights), keeps_packed.bin, tokens.txt and manifest.json
zipformer_p_arabic_v3.1.float8.mlpackage / zipformer_p_arabic_v3.1.float16.mlpackage v3.1 CoreML ANE packages (earlier interface). Converted from the released v3.1 ONNX, so they run the offset-4 network too, not the trained one: use v3.2
packing_front.json, packing_back.json state blob layout for the v3 and v3.1 CoreML packages, shared by v3 and v3.1 (identical architecture)
tokens.txt symbol table. Use it. The raw phoneme_units.json ids are offset by one against the CTC output layer (blank is 250, not 0); decoding without this table produces garbage
phoneme_units.json tokenizer unit inventory
ordered_quran_phonemes.json canonical phonemisation of all 6,236 ayat (for retrieval / grading)
quran_text2phoneme.json text-to-phoneme lookup used in evaluation
quran_per_eval.py, quran_wer_retrieval.py evaluation scripts
decode_with_confidence.py CTC decoding with per-symbol confidence
export_quran_streaming_onnx.py the exact export script that produced the ONNX files

Usage (ONNX, streaming)

The ONNX models are standard sherpa-onnx streaming-CTC zipformer2 exports (model_type=zipformer2, chunk 48 input frames; 32 for the _chunk16 files and 16 for the _chunk8 files). Feed 16 kHz mono, 80-dim kaldi fbank (povey window). The features must be kaldi fbank, not a mel-spectrogram: a slaney-mel front end roughly doubles the error rate.

import onnxruntime as ort, numpy as np
sess = ort.InferenceSession("zipformer_p_arabic_v3.int8.onnx")
# see quran_per_eval.py for the full streaming loop incl. cache tensors and fbank settings

Usage (CoreML, Apple Neural Engine)

v3.2: every op on the Neural Engine (recommended)

zipformer_p_arabic_v3.2.float16.mlpackage (fp16, 124 MB, iOS 18+) runs the whole encoder on the Apple Neural Engine: no op falls back to the CPU, and each call takes three tensors and returns two. It has four functions: front and back for a live stream, and front_b2 and back_b2 for two recordings per call (offline decoding). Per 480 ms chunk (61 fbank frames, advance 48), call front, then back:

Function Inputs Outputs
front x [1,1,61,80] fp16 fbank, zf_keepf [1,1,166,64], st [1,1,6930,64] input_tensor_1201_cast_fp16 [24,1,512], new_st
back input_tensor_1201_cast_fp16, zf_keepb [1,1,114,64], st [1,1,5804,64] log_probs [1,12,251], new_st
front_b2 x [2,1,61,80], zf_keepf [2,1,166,64], st [1,1,6930,128] input_tensor_1201_cast_fp16 [24,2,512], new_st
back_b2 input_tensor_1201_cast_fp16, zf_keepb [2,1,114,64], st [1,1,5804,128] log_probs [2,12,251], new_st
  • State. Every carried cache is packed into one fp16 tensor, st, per function. It starts at zero, and each call's new_st is the same function's next st (front and back carry their own).
  • Keeps. The attention and convolution masks are inputs: zf_keepf and zf_keepb are row min(counter / 24, 11) of keeps_packed.bin, where the frame counter starts at 0 and adds 24 per chunk. The file is little endian: int32 steps (12) and int32 inputs (2); for each input an int32 name length, the name and an int32 float count; then, for each step, each input's floats.
  • Features and decoding. 80-bin kaldi fbank at 16 kHz (povey window, dither 0, snip_edges false, high_freq -400), 1.05 s of silence at the end of an utterance, greedy CTC with blank 250 and tokens.txt.
  • Two recordings per call. Stream b is batch row b of x, the keeps and log_probs, with its own frame counter and keeps row. In st the rows keep their unbatched place at twice the width; the row and shape of every state are the pf_b_/pf_s_ constants (pfb_ for the batched pair) in the compiled model's model.mil. A stream starts a new recording by zeroing its part of st.
  • Speed. Bind every input and output to IOSurface-backed arrays created once, and pass the next state buffers as output backings.

Checked on an iPhone 15 Pro Max (iOS 26.6.1): every op of all four functions on the Neural Engine; on the 596-clip benchmark, 2 token edits against the v3.2 fp32 ONNX (both where fp32's top two classes are within 0.2 of each other), and the 35 parity clips token-identical to fp32; two recordings per call give the same log-probabilities as one, bit for bit; 10.4 ms per live chunk, 3.7 ms back to back.

The Quran Lab Swift SDK loads the same package from sdk/v3.2/, signed, with engine.zfs beside it: from the package's own weight file and that 4 MB sidecar, its exact CPU engine rebuilds the int8 weights of zipformer_p_arabic_v3.2.int8.onnx, checks every file against its SHA-256 and decodes bit for bit as ONNX Runtime does on ARM.

v3 and v3.1 packages (earlier interface)

The v3.1 packages were converted from the released v3.1 ONNX, so they run SwooshL at offset 4 like those files, not the network v3.1 was trained as (see v3.2 above); use the v3.2 package instead.

Both v3 and v3.1 ship two multifunction packages each (iOS 18+), identical in graph and interface and differing only in weight precision: *.float8.mlpackage (8-bit per-channel weights, fp16 compute, 63 MB, recommended) and *.float16.mlpackage (124 MB). Everything in this section applies to both models; they are interchangeable at the call site. Each contains the encoder as two functions, front and back, sharing one weight blob. They perform the same cache-aware streaming computation as the ONNX exports.

Why two functions. The ANE compiler has a per-program resource budget; this encoder exceeds it as one program, and the failure mode is silent CPU fallback. Split at the narrowest point of the network (a single 24x1x384 tensor crosses), both halves compile for the ANE (front 1,138 of 1,288 ops, back 1,405 of 1,572, measured via MLComputePlan). The same approach Apple uses to chunk its Stable Diffusion UNet.

Numerical safety, inherited from the v2 field debugging. Relative to a naive conversion the graph carries three fixes: a threshold-compare Swoosh overflow guard (fp16 hardware saturates at 65,504 and never produces inf, which silently corrupts an inf-compare guard), a max-subtracted log-softmax head (naive log-sum-exp saturates on the ANE), and no fp32 epsilon parameters (they abort ANE compilation). The mask-building reverse was also eliminated by reversing the ramp constant at export, removing a CPU island.

Verification. Streaming decode over 35 recitations (3 reciters), Mac Neural Engine execution (cpuAndNeuralEngine), each package against its own int8 ONNX: v3.1 is token-identical on 35 of 35 clips (554 tokens); v3 on 34 of 35 (519 tokens), and on that single clip the CoreML output matches the fp32 ONNX exactly while the int8 export is the outlier, so the CoreML packages are at least as faithful to the PyTorch weights as the int8 references. Confidence work should use margin_peak from decode_with_confidence.py (margins sampled at emission boundaries are not comparable across runtimes).

Metadata is embedded. Both packages carry the streaming contract in CoreML metadata, using the same keys as the ONNX exports (model_type, T, decode_chunk_len, left_context_len) plus the CoreML specifics (functions, call_order, cross_tensor, counter_input, blank_id, feature_type, fbank_options, recommended_compute_units), and the two packing manifests inlined as packing_front / packing_back, so the side JSON files are convenient, not required. Every input and output also carries a description, visible in Xcode's model viewer.

import coremltools as ct
md = ct.models.MLModel("zipformer_p_arabic_v3.float8.mlpackage",
                       skip_model_load=True).get_spec().description.metadata
cfg = dict(md.userDefined)          # T=61, decode_chunk_len=48, blank_id=250, ...

Interface, per 0.48 s chunk. The 97 cache tensors are packed by shape into a few fp16 blob tensors (sg_0 ...); packing_front.json / packing_back.json document each blob's members, shapes and concat axis. Carry blobs whole: every new_sg_k output feeds the next call's sg_k input, zero-initialised on the first call. The frame counter processed_lens (int32, starts at 0) is fed to both functions unchanged, then updated from front's new_processed_lens after the pair has run: back must see the pre-update value.

Call Inputs Outputs
front x (1, 61, 80) fbank chunk, processed_lens (1,) int32, blobs sg_0..sg_12 input_tensor_1439_cast_fp16 (24, 1, 384), new_processed_lens, new_sg_0..new_sg_12
back input_tensor_1439_cast_fp16 from front, processed_lens (pre-update), blobs sg_0..sg_11 log_probs (1, 12, 251), new_sg_0..new_sg_11
let cfg = MLModelConfiguration()
cfg.computeUnits = .cpuAndNeuralEngine
cfg.functionName = "front"
let front = try MLModel(contentsOf: compiledURL, configuration: cfg)
cfg.functionName = "back"
let back = try MLModel(contentsOf: compiledURL, configuration: cfg)
// per chunk: front.prediction -> back.prediction, carry new_sg_k -> sg_k

Integration notes: log_probs arrives with padded strides, so honour MLMultiArray.strides rather than indexing contiguously; MLMultiArray(shape:dataType:) returns uninitialised memory, so explicitly zero the state blobs on the first call; the iOS Simulator does not execute these packages correctly; validate on macOS or a physical device. On the v2 model of this same architecture the split float8 pair measured 6.8 ms per chunk on the ANE of an iPhone 15 Pro Max (70x realtime), with the ANE the fastest configuration outright; v3 timings are expected to match and will be updated when measured on device.

Training summary

Trained on about 5,400 effective hours per epoch for 10 epochs: professional complete-mushaf recitations (36 EveryAyah reciters, 1,161 additional reciters segmented from public recitation archives), real phone recordings, concatenated multi-ayah context windows, same-ayah repetition clips, and a broad-Arabic tier, with noise, reverberation, wind, speed and tempo augmentation. Labels are the deterministic Qur'anic phonetic script of quran-transcript (Hafs, murattal).

License

Released under the Quran-Lab No-Profit License, Version 1.2 (NPL-1.2); see LICENSE. In short: free to use, study, adapt and redistribute. The model itself is never for sale, so you may not charge for it, for access to it, or for a feature it powers. Your own labour stays your own: a teacher may charge for teaching and a developer may draw a salary. This is a condition on the work, not a ruling on anyone's livelihood.

Related

Downloads last month
357
Inference Providers NEW
This model isn't deployed by any Inference Provider. πŸ™‹ Ask for provider support

Spaces using Quran-Lab/zipformer_p-arabic-v3 2

Evaluation results

  • Phoneme Error Rate, held-out studio reciters on Quran-Lab/quranic-asr-benchmark (v1.1)
    self-reported
    1.430
  • Phoneme Error Rate, real phone audio on Quran-Lab/quranic-asr-benchmark (v1.1)
    self-reported
    3.650
  • Phoneme Error Rate, unseen reciter on Quran-Lab/quranic-asr-benchmark (v1.1)
    self-reported
    9.100