Nabr — Real-Time Recitation Correction API
Add AI recitation feedback with real-time tajweed correction and tilawa session streaming.
Description
Nabr - AI Quran Recitation Corrector, is a service where users can recite and get their recitation corrected in realtime. You will need a subscription to get an API Key and use this feature.
The API is a WebSocket that speaks binary MessagePack — every
message you send or receive is encoded/decoded with
@msgpack/msgpack.
The flow is:
- Connect to the WebSocket with your API key.
- Start a session by sending the
START_TILAWA_SESSIONmethod (once per session — when the page loads, for example). - Stream audio chunks with the
CHECK_TILAWA_AUDIOmethod while recording. - Receive
TILAWA_RESULTevents with correct words and mistakes in realtime. - Stop by sending the
RESET_TILAWA_SESSIONmethod.
Installation
npm install @msgpack/msgpackMicrophone audio is captured with the browser's built-in MediaRecorder API — no extra dependency needed.
Usage Example
import { useEffect, useRef, useState } from "react";
import { encode, decode } from "@msgpack/msgpack";
// Methods you send to the server
const WSMethods = {
START_TILAWA_SESSION: "START_TILAWA_SESSION",
CHECK_TILAWA_AUDIO: "CHECK_TILAWA_AUDIO",
RESET_TILAWA_SESSION: "RESET_TILAWA_SESSION",
} as const;
// Events you receive from the server
const WSEvents = {
QRC_CLIENT_CONNECTED: "QRC_CLIENT_CONNECTED",
TILAWA_SESSION_STARTED: "TILAWA_SESSION_STARTED",
TILAWA_RESULT: "TILAWA_RESULT",
QRC_ERROR: "QRC_ERROR",
} as const;
const WS_URL = "wss://api.qurani.ai?api_key=<YOUR_API_KEY>";
export default function QrcExample() {
const connection = useRef<WebSocket | null>(null);
const mediaRecorder = useRef<MediaRecorder | null>(null);
const [connected, setConnected] = useState(false);
const [recording, setRecording] = useState(false);
// 1. Connect and listen for messages
useEffect(() => {
const socket = new WebSocket(WS_URL);
socket.binaryType = "arraybuffer";
socket.addEventListener("open", () => setConnected(true));
socket.addEventListener("close", () => setConnected(false));
socket.addEventListener("message", (event: MessageEvent<ArrayBuffer>) => {
// Every message is MessagePack-encoded binary
const message = decode(new Uint8Array(event.data)) as {
event: string;
[key: string]: unknown;
};
switch (message.event) {
case WSEvents.QRC_CLIENT_CONNECTED:
// Connection acknowledged — message.websocket_id, message.credits_balance
break;
case WSEvents.TILAWA_SESSION_STARTED:
// Session is ready — message.session_id, message.exit_code
break;
case WSEvents.TILAWA_RESULT:
// Realtime feedback on the recitation:
// message.verse_index / message.word_index -> current position
// message.correct_words -> words read correctly
// message.skipped_words -> words that were skipped
// message.tajweed_mistakes -> tajweed errors
// message.letter_mistakes -> letter pronunciation errors
console.log("result", message);
break;
case WSEvents.QRC_ERROR:
console.error(`${message.code}: ${message.message}`);
break;
}
});
connection.current = socket;
return () => socket.close();
}, []);
// 2. Start the session, then stream microphone audio
const startRecording = async () => {
const socket = connection.current;
if (socket?.readyState !== WebSocket.OPEN) return;
// Start a tilawa session (send once per session)
socket.send(
encode({
method: WSMethods.START_TILAWA_SESSION,
chapter_index: 1, // surah number
verse_index: 1, // starting verse
word_index: 1, // starting word
hafz_level: 1, // 1-3
tajweed_level: 1, // 1-3
}),
);
// Record the microphone and send each chunk to the server
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const recorder = new MediaRecorder(stream);
mediaRecorder.current = recorder;
recorder.ondataavailable = async (event) => {
if (
event.data.size > 0 &&
connection.current?.readyState === WebSocket.OPEN
) {
const buffer = await event.data.arrayBuffer();
connection.current.send(
encode({
method: WSMethods.CHECK_TILAWA_AUDIO,
audio: new Uint8Array(buffer),
}),
);
}
};
recorder.onstop = () => {
// Release the microphone
stream.getTracks().forEach((track) => track.stop());
};
recorder.start(250); // emit an audio chunk every 250ms
setRecording(true);
};
// 3. Stop recording and reset the session on the server
const stopRecording = () => {
mediaRecorder.current?.stop();
mediaRecorder.current = null;
if (connection.current?.readyState === WebSocket.OPEN) {
connection.current.send(
encode({ method: WSMethods.RESET_TILAWA_SESSION }),
);
}
setRecording(false);
};
return (
<button
disabled={!connected}
onClick={recording ? stopRecording : startRecording}
>
{recording ? "Stop recording" : "Start recording"}
</button>
);
}Types
Payloads you send (always MessagePack-encoded):
type StartTilawaSessionPayload = {
method: "START_TILAWA_SESSION";
chapter_index: number;
verse_index: number;
word_index: number;
hafz_level: number; // 1-3
tajweed_level: number; // 1-3
};
type CheckTilawaAudioPayload = {
method: "CHECK_TILAWA_AUDIO";
audio: Uint8Array;
};
// Signals recording stopped; server flushes audio and resets the session
type ResetTilawaSessionPayload = {
method: "RESET_TILAWA_SESSION";
};Events you receive (decode each message with MessagePack):
type QRCClientConnected = {
event: "QRC_CLIENT_CONNECTED";
websocket_id: string;
credits_balance?: number | null;
};
type TilawaSessionStarted = {
event: "TILAWA_SESSION_STARTED";
exit_code: number; // 6 = success
session_id: string;
};
type QRCWord = {
chapter: number;
verse: number;
word: number;
};
type Mistake = {
chapter: number;
verse: number;
word: number;
letter?: number;
speachLike: string;
message: string;
errorCode?: string | null;
};
type TilawaResult = {
event: "TILAWA_RESULT";
exit_code: number; // 6 = success, 8 = surah finished
chapter_index: number;
verse_index: number;
word_index: number;
correct_words: QRCWord[];
skipped_words: QRCWord[];
tajweed_mistakes: Mistake[];
letter_mistakes: Mistake[];
text: string; // recognized text from ASR
};
type QRCError = {
event: "QRC_ERROR";
websocket_id: string;
code: string;
message: string;
};Playground
Try it out in the playground below, you can also use the code above to implement it in your own project.
بِسۡمِ ٱللَّهِ ٱلرَّحۡمَٰنِ ٱلرَّحِيمِ 1ٱلۡحَمۡدُ لِلَّهِ رَبِّ ٱلۡعَٰلَمِينَ 2ٱلرَّحۡمَٰنِ ٱلرَّحِيمِ 3مَٰلِكِ يَوۡمِ ٱلدِّينِ 4إِيَّاكَ نَعۡبُدُ وَإِيَّاكَ نَسۡتَعِينُ 5ٱهۡدِنَا ٱلصِّرَٰطَ ٱلۡمُسۡتَقِيمَ 6صِرَٰطَ ٱلَّذِينَ أَنۡعَمۡتَ عَلَيۡهِمۡ غَيۡرِ ٱلۡمَغۡضُوبِ عَلَيۡهِمۡ وَلَا ٱلضَّآلِّينَ 7