Tasbeeh keyword-spotting heads (Arabic dhikr counting)
Eight tiny (~860 KB each) ONNX binary classifiers that fire on spoken dhikr phrases over
continuous open-mic audio, in the openWakeWord
architecture: frozen Google speech_embedding features (96 dims, 0.125 s/frame) ->
per-phrase DNN head. Five short heads use the standard 2 s window (16 frames); three
longer Azkar use wide windows (12/56/32 frames) sized from measured clip durations.
Designed as an on-device tasbeeh counter: VAD/energy gate -> sliding windows -> per-phrase
score >= threshold -> counter with a refractory period.
Phrases and thresholds
| phrase | Arabic | input frames | hold-out recall@0.5 | FP/hr | best threshold |
|---|---|---|---|---|---|
| subhanallah | ุณุจุญุงู ุงููู | 16 (2 s) | 0.93 | 0.00 | 0.3 |
| alhamdulillah | ุงูุญู ุฏ ููู | 16 (2 s) | 0.86 | 0.00 | 0.3 |
| allahuakbar | ุงููู ุฃูุจุฑ | 16 (2 s) | 0.93 | 0.00 | 0.3 |
| la_ilaha_illa_allah | ูุง ุฅูู ุฅูุง ุงููู | 16 (2 s) | 0.93 | 0.00 | 0.3 |
| astaghfirullah | ุฃุณุชุบูุฑ ุงููู | 16 (2 s) | 0.93 | 0.00 | 0.3 |
| la_hawla_wala_quwwata | ูุง ุญูู ููุง ููุฉ ุฅูุง ุจุงููู | 12 (~1.5 s) | 0.87 | 0.00 | 0.5 |
| la_ilaha_illa_allah_wahdahu | ูุง ุฅูู ุฅูุง ุงููู ูุญุฏู ูุง ุดุฑูู ูู ูู ุงูู ูู ููู ุงูุญู ุฏ ููู ุนูู ูู ุดูุก ูุฏูุฑ | 56 (~7 s) | 0.56 | 0.00 | 0.5 |
| la_ilaha_illa_anta | ูุง ุฅูู ุฅูุง ุฃูุช ุณุจุญุงูู ุฅูู ููุช ู ู ุงูุธุงูู ูู | 32 (~4 s) | 0.54 | 0.00 | 0.5 |
Rapid-tasbeeh detection on 2 noisy simulated streams x 25 utterances each (short heads): subhanallah 34/50, alhamdulillah 20/50, allahuakbar 24/50, la_ilaha_illa_allah 40/50, astaghfirullah 36/50. The wide-window heads were evaluated on hold-out only (no rapid sim).
Overlap note: the long dhikr ูุง ุฅูู ุฅูุง ุงููู ูุญุฏู ูุง ุดุฑูู ููโฆ contains the short phrase ูุง ุฅูู ุฅูุง ุงููู as its opening. When the 56-frame head fires, the app should count that dhikr and suppress the 16-frame head for that burst.
Usage
import onnxruntime as ort
import numpy as np
sess = ort.InferenceSession("tasbeeh_subhanallah.onnx", providers=["CPUExecutionProvider"])
# feats: (N, 16, 96) windows from openwakeword's AudioFeatures().embed_clips
scores = sess.run(None, {"input": feats.astype(np.float32)})[0].reshape(-1)
Wide-window heads take the same 96-dim features with more frames per window:
(N, 12, 96), (N, 56, 96), (N, 32, 96) respectively โ slide a window of that many
frames over the stream (stream hop is 0.08 s/frame; window span = frames x 0.125 s).
For counting in an app: score windows every 80 ms over a VAD-gated stream, fire on score >= threshold (0.3 short heads, 0.5 wide heads), and use a refractory of ~1.2 s (the rapid-tasbeeh simulation shows the 2 s analysis window fires 2-3 times per single utterance with a 0.45 s refractory โ a ~1.2 s refractory collapses those to one count).
Training data and recipe
- Positives: short heads โ 449 synthetic clips (5 phrases x 3 text variants x 7 Arabic dialects x 4 speeds); wide heads โ 252 regenerated clips (3 phrases x 7 dialects x 4 speeds x 3 seeds). All synthesized with SWivid/Habibi-TTS (F5-TTS), then RIR-reverberated and noise-augmented (8 rounds). Dataset: mZahran001/tasbeeh-kws-data. (Wide heads were first trained on 21 clips/phrase and collapsed to 0 recall โ the 84-clip-per-phrase volume restored recall.)
- Adversarial negatives: similar-but-not-dhikr clips from the same TTS pipeline.
- General negatives: ~2.7k 2 s segments of Common Voice Arabic (validation split, dhikr-containing sentences filtered out) + openWakeWord's FP validation feature set (481k windows) for false-positive training.
- Training: openWakeWord
Model.auto_train, 25k steps per phrase, binary cross-entropy, balanced validation mix, and a post-train collapse guard that retries with gentler FP pressure if a head stops firing on its own training positives.
Caveats (read before shipping)
- All evaluation is synthetic. Recall numbers come from TTS hold-out clips and simulated streams; real-device accuracy will differ. Before production, record 50-100 real utterances per phrase on target phones and re-tune the thresholds.
- The two wide-window heads (~0.55 recall) are the weakest โ long duas on 84 synthetic clips are the hardest case; expect them to improve most from real-recorded data.
- Trained heads assume the exact
speech_embeddingfeature pipeline used here (openwakeword >= 0.6 from GitHub main).
Files
tasbeeh_<phrase>.onnxโ 5 short heads (input: (N, 16, 96) float32)dhikr_<phrase>_<frames>.onnxโ 3 wide-window heads (input: (N, 12|56|32, 96) float32)eval_report.jsonโ short heads: hold-out recall, adversarial FP rate, threshold sweep, rapid-tasbeeh simulation resultseval_report_new.jsonโ wide heads: hold-out recall, FP/hr, threshold