Scriptune
Hear a hymn or a Bible verse and find out what it is: one TypeScript API behind a website, an iPhone app and an Android app, with Whisper speech recognition on a self-hosted server and on the phone itself.
- 51
- API routes
- 29
- Prisma models
- 7
- Bible translations
- 3
- Search stages
- Role
- Author
- Year
- 2026
- Status
- Open source
- Type
- Hymn and scripture recognition
01Overview
Scriptune listens to a hymn the choir has started or a verse the preacher has quoted, works out what it was, and opens the words: the full hymn with its number in the hymnal, or the verse in its chapter, with the passages and hymns connected to it.
I built it on my own and released it under the MIT licence. It is a Zudojs modular-monolith API over PostgreSQL, a Next.js website that installs as a PWA, an Expo app for iOS and Android, and a Python service that runs OpenAI Whisper. Both clients import one shared, typed contracts package.
The hard part is not the speech recognition. It is turning a noisy, half-heard transcript into the right verse among seven Bible translations, or the right hymn among 1,200, without asking the database for expensive fuzzy work it does not need.
02The problem
Churchgoers catch scripture and hymns in fragments. A verse is quoted mid-sermon without a reference; a hymn starts and the number is not on the board. Searching the web for a half-remembered line works badly when the line was sung, a word was misheard, or it was in Yoruba.
I did not want church audio going to a commercial speech API by default. Transcription had to run on hardware the operator controls, and a recording had to be discarded once it had been turned into words.
It also has to work in a pew, sometimes with no signal at all. That means the texts, the search and, ideally, the listening model have to live on the phone.
03Architecture
- 01
One API, nine modules
The API is a Zudojs modular monolith with nine modules: http, database, bible, hymns, search, transcription, recognition, identity and library. Each registers its own providers, command and query handlers and routes, and dependencies point strictly downward. It serves 51 routes, documented as OpenAPI at /docs, over 29 Prisma models.
- 02
Three-stage search
Verses and hymns are searched in the same three stages. First, every word present, as one GIN lookup against a generated tsvector. If that finds nothing, any pair of the stronger words, which is still an index lookup. If that also finds nothing and the phrase is at least 12 characters, pg_trgm word similarity, which rechecks thousands of rows. Candidates are then scored for confidence and the top five returned.
- 03
Whisper behind an interface
Speech-to-text sits behind a TranscriptionProvider interface with three implementations: the Whisper service, a fake for tests, and an unavailable provider that switches audio off deliberately. The Python service is FastAPI over openai-whisper. It runs one transcription at a time behind a semaphore, answers 503 with Retry-After when busy instead of queueing without limit, and checks each model download against its SHA-256 before loading it.
- 04
Hymns and scripture linked by their words
An import-time job finds where a hymn quotes the King James Version by looking for runs of four shared words, scored by how rare those words are in the Bible and discarded if the run appears in more than twelve verses. So "the print of the nails" counts and "O Lord our God" does not. A second job pairs each hymn with the hymns most like it, by rarity-weighted wording plus shared scripture.
- 05
Offline on the phone
The API exports a whole translation or hymnal as one response that clients may cache for a day. The Expo app stores it in SQLite with FTS5 tables, so reading, search and typed identification work with no connection. whisper.rn adds on-device transcription with a model downloaded on request rather than bundled, so native builds can listen offline too.
- 06
Shared contracts
@scriptune/contracts holds the DTO types, library keys, legal text and fetch client that both the Next.js site and the Expo app import. A change to a response shape fails both clients' typecheck in CI rather than one of them at runtime.
- 07
CI and native builds
GitHub Actions runs the API typecheck and tests against a PostgreSQL 17 service, the contracts typecheck, the web lint, typecheck and production build, and the mobile typecheck, lint and Android bundle. A second workflow builds the Android APK and the iOS app on GitHub's runners, on demand or from a mobile-v* tag.
04Key decisions
Cheap stages first
Each more expensive search stage runs only when the one before it found nothing. Trigram similarity is the stage that forgives a misheard word, and it is also the slow one, so it is the last resort rather than the default.
Fold accents in the index, not the app
Yoruba tone marks meant a query transcribed as "Olorun" never matched a verse stored as "Ọlọ́run", and fell through to the slow fuzzy stage. A Postgres function strips combining marks after NFKD normalisation, and both the stored search vectors and the incoming query pass through it, so Yoruba stays on the fast index path.
Self-hosted speech, no stored audio
Whisper runs on the operator's server or on the phone; no third-party speech service is involved. A recognition attempt records the transcript, candidates, timings and the audio's type and size, never the audio itself. If the transcriber is down, only the microphone flow fails; typed identification never touches it.
Work out the connections ahead of time
Scripture links and related hymns are computed by batch jobs and stored as rows. Comparing a hymnal's words against the whole Bible is batch work; serving the answer should be a few indexed reads.
05Stack
- TypeScript
- Zudojs
- Prisma 7
- PostgreSQL 17
- Redis 7
- Python
- FastAPI
- OpenAI Whisper
- Next.js 16
- React 19
- Tailwind 4
- Expo 57
- React Native
- SQLite
- whisper.rn
- Docker
- GitHub Actions