I wanted to be able to open a recording of the Qur’an and jump to a specific page of the mushaf. Not a verse in a list, not a chapter, an actual page, the way you would open a printed copy. The recordings I have are in the Warsh riwaya, which matters later, and they are long continuous files with no useful metadata.

So I built a tool. It started as a small script and turned into a desktop application in Python and tkinter, and along the way it taught me more about what “applied AI” means in practice than a year of reading about it.

First attempt: verse-level, and why it was the wrong unit

The obvious design is to find the timestamp of every verse. The text is known, the structure is known, so it should be a solved problem.

It is not, for a reason that has nothing to do with the model. Recitation does not stop at verse boundaries in a way that maps cleanly to audio silence. Pauses fall where the reciter breathes and where the rules of recitation place them, which is frequently not at the end of a verse. Trying to force verse-level boundaries produced timestamps that were technically present and practically wrong: they would land mid-phrase, and the playback experience was worse than no index at all.

The second version indexes at page level. 604 timestamps for the whole mushaf, one per page. This is a much smaller and much more useful target. It matches how people actually navigate, the boundaries are less contested, and the error tolerance is human-sized: being half a second early at a page start is invisible, while being half a second early on a verse is jarring.

Choosing the right unit of indexing was the single biggest improvement in the project, and it involved no machine learning at all.

Transcription is the easy half

Whisper does the transcription. It is remarkably good at this and I will not pretend I contributed anything there beyond calling it correctly. Chunking, language hint, and patience.

What Whisper gives you is an approximate transcript with timestamps. What it does not give you is the canonical text, and it cannot, because it is transcribing what it hears and the canonical text is a fixed thing with specific orthography. Diacritics vary. Spelling of the transcript will not match the reference. And because I am working in the Warsh riwaya, there are places where the recited text legitimately differs from the more commonly available reference text, which means a naive string comparison will report a mismatch that is not an error.

The real problem is alignment

So the actual engineering problem is this: given an approximate transcript with timestamps, and a known reference text, find where in the audio each page of the reference begins.

This is fuzzy matching over a sequence, and rapidfuzz does the heavy lifting. The approach that worked was to normalise aggressively — strip diacritics, normalise the letter forms that vary, collapse whitespace — and then slide the reference page text against the transcript window, scoring similarity and taking the best position.

Normalisation is where the domain knowledge lives. Every rule I added about which characters are equivalent improved the match rate more than any tuning of the matching algorithm did. That is a pattern I have since seen repeatedly: in applied AI projects, the preprocessing that encodes what you know about your data is usually worth more than the model choice.

The human in the loop is not an admission of defeat

Automatic alignment gets most pages right and some pages wrong, and the wrong ones cluster in predictable places: the start of a recording where the reciter has an introduction, sections where audio quality drops, and a handful of pages where the transcript simply went astray.

I spent a while trying to push the automatic accuracy higher, then gave up and built a manual alignment interface instead. A tkinter canvas showing the waveform, the candidate boundary, the page text, and arrow keys to nudge. Fixing a bad page takes about fifteen seconds.

This was the right call and I resisted it for too long. Three hundred hours of engineering to eliminate two hours of human correction is a bad trade, and it is a trade our industry makes constantly because the automatic version feels more impressive.

What I would tell someone starting a similar project

Pick the coarsest unit that is still useful. Precision you do not need costs you accuracy you do need.

Budget most of your time for the join, not the model. Calling the model is a line of code. Reconciling its output with your ground truth is the project.

Encode your domain knowledge in normalisation. It is boring, unglamorous, and it is where the wins are.

Build the correction UI early. You will need it, and having it early lets you ship at 90% instead of waiting for 99%.

Keep the reference data separate and version it. I did not do this at first and paid for it.

I am not a machine learning engineer. I size infrastructure for a living and I write Python on weekends. That is exactly why this project was instructive: almost everything that was hard about it was ordinary software engineering, applied carefully to a domain I happen to know well. Most useful AI work, as far as I can tell, looks like that.