@tilawa/core
v0.2.1
Published
Offline Quran verse recognition. Give it 16kHz audio, get surah:ayah. Pure TypeScript core with a pluggable ONNX runtime seam — works in web, node, and React Native.
Maintainers
Readme
Tilawa
Formerly called offline-tarteel.
Offline Quran recognition. Give it 16 kHz mono audio, get back surah:ayah. Fully on-device — web, mobile, or node, no network at inference time.
@tilawa/core is pure TypeScript with zero native dependencies. You inject the ONNX runtime.
Licence split: package code is MIT. The Zipformer model and phoneme corpus are NPL-1.2 (non-commercial, share-alike). FastConformer assets are MIT / NVIDIA CC-BY-4.0. Details: NOTICE.md.
Two engines ship in the box. The default is Zipformer — streaming Zipformer2-CTC over a 251-token tajweed-phoneme vocabulary. FastConformer (text CTC) is still there under its original API.
Install
npm i @tilawa/core
# plus the onnxruntime for your platform (you own this dep):
npm i onnxruntime-web # browser / WASM
npm i onnxruntime-node # node
npm i onnxruntime-react-native # React NativeDefault-engine assets from release v0.3.0:
base=https://github.com/yazinsai/tilawa/releases/download/v0.3.0
curl -L -O "$base/zipformer_interp_gentle_a05.int8.onnx" # 66 MB
curl -L -O "$base/zipformer_quran.json" # 5.5 MB, NPL-1.2
# optional — Arabic text on verse_match events
curl -L -O https://github.com/yazinsai/tilawa/releases/download/v0.2.0/quran.jsonThe model's I/O manifest is bundled (DEFAULT_ZIPFORMER_IO). quran.json is display text only; matching works without it.
Browser
import * as ort from "onnxruntime-web";
// `onnxruntime-web/wasm` works too
import { createRecognitionSession } from "@tilawa/core";
// Vite copies `*.wasm` into the bundle by default. A raw <script type=module>
// or a bundler that doesn't should set:
// ort.env.wasm.wasmPaths = "https://cdn.jsdelivr.net/npm/[email protected]/dist/";
const session = await createRecognitionSession({
ort,
model: () => fetch("/zipformer_interp_gentle_a05.int8.onnx").then((r) => r.arrayBuffer()),
corpus: () => fetch("/zipformer_quran.json").then((r) => r.json()),
quran: () => fetch("/quran.json").then((r) => r.json()), // optional
onEvent: (msg) => {
if (msg.type === "verse_match") console.log(`${msg.surah}:${msg.ayah}`, msg.verse_text);
},
});
for await (const chunk of micChunks) await session.feed(chunk);
const final = await session.stop();
session.reset();Passing ort + model picks the provider automatically: ["wasm"] under onnxruntime-web, ["cpu"] under onnxruntime-node. Override with executionProviders. On web we also set ort.env.wasm.numThreads = 1 unless you already set it — pthread init hangs in workers without COOP/COEP; single-thread is what the demo ships.
createZipformerSession(opts) is the same thing without the engine switch, and returns the richer ZipformerSession (transcript, verses, engineState, …).
Node
import { readFile } from "node:fs/promises";
import * as ort from "onnxruntime-node";
import { createRecognitionSession } from "@tilawa/core";
const session = await createRecognitionSession({
ort,
model: () => readFile("zipformer_interp_gentle_a05.int8.onnx"),
corpus: async () => JSON.parse(await readFile("zipformer_quran.json", "utf8")),
quran: async () => JSON.parse(await readFile("quran.json", "utf8")),
onEvent: (msg) => {
if (msg.type === "verse_match") console.log(`${msg.surah}:${msg.ayah}`);
if (msg.type === "raw_transcript") console.log(msg.text);
},
});
const CHUNK = Math.round(0.3 * 16000); // 300 ms
for (let i = 0; i < pcm16k.length; i += CHUNK) {
await session.feed(pcm16k.subarray(i, i + CHUNK));
}
const final = await session.stop();
session.reset();React Native
RN can't hand the model to ORT as an ArrayBuffer — bundle the .onnx as an asset, copy it to the documents dir, create the session from the path, and pass that session in with the runtime's Tensor:
import * as ort from "onnxruntime-react-native";
import { createZipformerSession } from "@tilawa/core";
const session = await createZipformerSession({
session: await ort.InferenceSession.create(modelPath),
Tensor: ort.Tensor,
corpus: () => loadJsonAsset("zipformer_quran.json"),
quran: () => loadJsonAsset("quran.json"),
});Walkthrough: examples/react-native.md. Copy-paste runners: examples/.
Alternate engine: FastConformer
Pick it for one-shot transcribe(), a raw Arabic transcript, or MIT-only assets. Download from release v0.2.0:
base=https://github.com/yazinsai/tilawa/releases/download/v0.2.0
curl -L -O "$base/fastconformer_full_mixed.onnx"
curl -L -O "$base/vocab.json"
curl -L -O "$base/quran_ctc_tokens.json"Write a SessionRunner that owns ort, then hand it to createTilawaSession with { vocab, quranCtcTokens, quran }. Missing keys throw Error("fastconformer engine requires assets: vocab, ctcTokens, quran ...") before anything is read.
import * as ort from "onnxruntime-web";
import { createTilawaSession, type SessionRunner } from "@tilawa/core";
async function createWebSessionRunner(modelBuffer: ArrayBuffer): Promise<SessionRunner> {
const session = await ort.InferenceSession.create(modelBuffer, {
executionProviders: ["wasm"],
});
return {
async run(audio) {
const input = new ort.Tensor("float32", audio, [1, audio.length]);
const length = new ort.Tensor("int64", BigInt64Array.from([BigInt(audio.length)]), [1]);
const results = await session.run({ audio_signal: input, length });
const output = results[session.outputNames[0]];
const [, timeSteps, vocabSize] = output.dims as number[];
return { logprobs: output.data as Float32Array, timeSteps, vocabSize };
},
};
}
const session = createTilawaSession(await createWebSessionRunner(modelBuffer), {
vocab,
quranCtcTokens,
quran,
});
const pred = await session.transcribe(audioFloat32);
// { surah: 1, ayah: 1, ayah_end: 3, score: 0.92, transcript: "..." }Same runner shape on node (onnxruntime-node) and RN (create from a file path). FastConformer has no stop() — it finalizes on trailing silence. Wrap it in createRecognitionSession({ engine: "fastconformer", runner, assets }) for the uniform feed() / stop() / reset() surface.
Verse events
Both engines emit the same WorkerOutbound union — via onEvent / onOutput, and as the return value of feed() / stop():
| msg.type | Meaning | Key fields |
|---|---|---|
| verse_match | Confident match for the current verse | surah, ayah, verse_text, surah_name, confidence, surrounding_verses |
| verse_candidate | Ranked candidates before lock-in | candidates[], stable, final_flush |
| word_progress | Word-level alignment within a verse | surah, ayah, word_index, total_words, matched_indices |
| raw_transcript | Accumulated transcript so far (and again on stop()) | text, confidence |
| final_sequence | Full ordered sequence when recitation ends | verses[], confidence |
API
createRecognitionSession(options)
engine defaults to "zipformer" (DEFAULT_ENGINE). Pass engine: "fastconformer" with { runner, assets }. Returns feed(), stop() / flush(), reset(), plus zipformer / fastconformer (the other is null).
createZipformerSession(options) → ZipformerSession
{ ort, model }— runtime namespace + model bytes (or a loader). Providers:["wasm"]under onnxruntime-web,["cpu"]under onnxruntime-node.{ session, Tensor }— anInferenceSessionyou created plus that runtime'sTensor. RN shape.
| Option | Default | Purpose |
|---|---|---|
| corpus | required | Parsed zipformer_quran.json, or a loader |
| quran | empty | Arabic text for verse_match |
| io | DEFAULT_ZIPFORMER_IO | Override only for your own export |
| executionProviders | auto | See above |
| onEvent | — | Verse events, same order feed() / stop() return them |
| minWordFraction | 0.5 | Fraction of an ayah's words that must land |
| enableFallback | true | Whole-ayah search when nothing locked |
| tailSeconds | 2.0 | Silence stop() appends to flush the CTC tail |
Also: transcript, tallies / verses, engineState, config.
createTilawaSession(runner, assets, options?) → TilawaSession
assets: { vocab, quranCtcTokens, quran, blankId? }. transcribe() / transcribeRaw() / feed() / reset() / setConfig() / getConfig(). Streaming presets: "conservative" / "balanced" / "aggressiveAdvance".
audio on SessionRunner.run is borrowed, not owned — treat it as read-only.
Models
| | Zipformer (default) | FastConformer |
|---|---|---|
| File | zipformer_interp_gentle_a05.int8.onnx (66 MB) | fastconformer_full_mixed.onnx (88 MB) |
| Input | 16 kHz mono Float32Array, streamed | same, preprocessing in-graph |
| Recall / Precision / SeqAcc | 100% / 100% / 100% on v1 (53/53) and v2 (43/43) | 100% / 100% / 100% on v1 (53/53) |
| Licence | NPL-1.2 non-commercial share-alike | NVIDIA CC-BY-4.0 |
This repository
Live demo: web/frontend (?engine=fastconformer to switch). Bake-off writeups: lab/EXPERIMENTS.md.
Zipformer models, vocabulary, and the phoneme corpus derive from Quran-Lab/zipformer_p-arabic-v3 and alketab's ملقّن القرآن. They are NPL-1.2 and are not covered by this repo's MIT licence. NOTICE.md.
