"Convey from me, even one verse." β Prophet Muhammad ο·Ί (Bukhari)
Auto-generate cinematic, karaoke-synced Quran reels and publish them to TikTok, Instagram, Facebook & YouTube β on a schedule, hands-free.
Pick nothing. A scheduler chooses a random surah + passage, pulls the exact recitation, renders a vertical video with word-by-word highlighting over a calm background, and posts it several times a day.
π English Β· Ψ§ΩΨΉΨ±Ψ¨ΩΨ© Β· FranΓ§ais Β· Ψ§Ψ±Ψ―Ω Β· Bahasa Indonesia Β· TΓΌrkΓ§e Β· Bahasa Melayu Β· বাΰ¦ΰ¦²ΰ¦Ύ Β· ΩΨ§Ψ±Ψ³Ϋ Β· EspaΓ±ol
βΆ See it live: @eQurany on TikTok β every video there is auto-generated by this project.
βΆ Play with sound
| classic | glass | noor |
demo-classic.mp4 |
demo-glass.mp4 |
demo-noor.mp4 |
Three built-in templates β switch with TEMPLATE or in the panel/menu. Every reel varies its background, and glass gets a unique waveform per video.
- π¬ Cinematic 1080Γ1920 reels β full-bleed stock photo background (Pexels/Unsplash) with a strong legibility overlay, subtle drifting particles, and a slow Ken-Burns drift.
- π¨ Three visual templates β classic (photo + scrim), glass (one persistent frosted glassmorphism card with a live audio waveform), and noor (warm "Divine Light" β golden halo + gilded numerals). Set
TEMPLATEor switch it in the panel/menu. - π Bismillah intro β a passage that starts at ayah 1 always opens with the reciter's own Bismillah (recited, in their voice); optionally prepend it before every passage (
BASMALA=always). At-Tawbah and Al-Fatiha are handled correctly. - π€ Karaoke word-fill β each word brightens in sync with the recitation (rightβleft), so viewers follow along.
- ποΈ Authentic Arabic typography β full Uthmani text with correct shakl in the clean modern Mada face; headers in Aref Ruqaa calligraphy.
- π― Exact sync, zero AI β audio comes from everyayah.com as per-ayah files, so each ayah's timing is exact and free (no transcription).
- π Auto content selection β random surah + a random consecutive passage (configurable length); short surahs render in full.
- π Safe, tasteful backgrounds β a curated 50-keyword pool (mosques, nature, sea, skyβ¦) plus a filter that drops any photo containing people or anything unsuitable. Pick your source: Pexels, Unsplash, or your own local image folder.
- π₯οΈ Interactive command center β run
tabligh(no arguments) for a stunning terminal UI to generate reels, start/stop the panel, manage the queue, browse history, edit settings, and run a health check. Localized EN/AR/FR. - ποΈ Signature outro β the passage fades out and a αΉ£alawΔt (with your logo) glides up over the same scene, then the whole video fades to black.
- π€ Multi-platform publishing β TikTok, Instagram Reels, Facebook Reels and YouTube Shorts via Buffer, toggled per platform by env.
- ποΈ Self-hosted control panel β a password-protected web UI to manage every setting, generate/preview/post a reel, queue passages, and browse history + analytics β all live, no redeploy. Localized (EN/AR/FR, full RTL).
- β° Set-and-forget scheduler β an always-on process posts N times a day in your timezone.
- π§Ή Disk-friendly β local files are wiped right after posting; cloud objects are pruned automatically.
- π³ One-container deploy β Dockerfile + works great on Coolify, Fly, Railway, or any Docker host.
config / random pick
β
βΌ
Quran text + translation βββΊ everyayah per-ayah audio (exact timing)
(alquran.cloud) β
β βΌ
ββββββββββββββΊ TimedAyah[] βββΊ background (Pexels/Unsplash, person-filtered)
β
βΌ
Chromium renders animated frames (karaoke, particles, outro)
β
βΌ
ffmpeg β MP4 (1080Γ1920) + recitation + silent outro
β
βΌ
object storage (public URL) βββΊ Buffer βββΊ TikTok / IG / FB / YT
β
βΌ
cleanup (local now, cloud after ingest)
Requirements: Node β₯ 20 and ffmpeg on your PATH. (Chromium is downloaded automatically by Puppeteer.)
# The short way β no clone, no build:
npx tabligh # interactive command center
npm i -g tabligh # or install it properly, then: tabligh render β¦
# Or from source:
git clone https://github.com/911RS/tabligh.git
cd tabligh
npm install
cp .env.example .env # fill in what you need (see below)
# Render a specific passage to work/β¦/reel.mp4 (no publishing)
npm start render -- --surah 112 --from 1 --to 4 --reciter husary --translation en.sahih
# Render a random passage
npm start random
# Everything the CLI can do
npm startThe finished reel.mp4 (plus the timed ir.json) lands in work/<surah>_<range>_<reciter>__<tag>/.
Everything is driven by environment variables (.env). All are optional except where a feature needs a key.
| Variable | Purpose | Default |
|---|---|---|
TEMPLATE |
Reel visual style: classic / glass / noor |
classic |
BASMALA |
Bismillah intro: off (only at ayah 1) / always (every passage) |
off |
BACKGROUND_SOURCE |
auto / pexels / unsplash / local |
auto |
BACKGROUND_LOCAL_DIR |
Folder of your own portrait images (when source = local) |
(empty) |
PEXELS_API_KEY / UNSPLASH_ACCESS_KEY |
Stock photo backgrounds | (unset β gradient fallback) |
BUFFER_ACCESS_TOKEN |
Buffer API token for publishing | (unset β no publishing) |
BUFFER_TIKTOK_CHANNEL_IDS |
Comma-separated TikTok channel ids | (empty) |
BUFFER_INSTAGRAM_CHANNEL_IDS |
Instagram Reels channel ids | (empty) |
BUFFER_FACEBOOK_CHANNEL_IDS |
Facebook Reels channel ids | (empty) |
BUFFER_YOUTUBE_CHANNEL_IDS |
YouTube Shorts channel ids | (empty) |
MINIO_* |
S3-compatible storage (public bucket Buffer fetches from) | bucket tabligh, port 9000 |
TZ / PUBLISH_TIMES |
Timezone + times of day to auto-post | Africa/Tunis / 07:00,13:00,19:00 |
KARAOKE_ENABLED |
Word-by-word fill synced to recitation | true |
TEXT_FILL_COLOR |
Color of the recited (filled) text | #ffffff |
WATERMARK_ENABLED / WATERMARK_HANDLE |
Corner logo watermark (assets/logo.png) |
true / (empty) |
FULL_SURAH_MAX_AYAHS |
Surahs this short render in full | 7 |
RANDOM_MIN_AYAHS / RANDOM_MAX_AYAHS |
Passage length for random mode | 5 / 10 |
MAX_VIDEO_SECONDS |
Cap recitation length (excl. outro); trims trailing ayahs to fit (overrides min) | 0 (no limit) |
RETENTION_DAYS / MINIO_RETENTION_HOURS |
Cleanup windows | 7 days / 24 h |
PORT / TRIGGER_TOKEN |
HTTP server + secret for the trigger endpoint | 1998 / (unset β disabled) |
PANEL_ENABLED |
Serve the control panel (false = headless, scheduler only) |
true |
UI_LANG |
Interactive terminal-menu language (en / ar / fr) |
en |
See .env.example for the full annotated list. These values seed the store on first run only β after that, manage settings live in the panel or the tabligh menu.
Reciters: husary, minshawy, abdulbasit, hudhaify, ayyoub, shuraym, husary-muallim, tunaiji β or any raw everyayah folder. See src/quran/reciters.ts.
Translations: any alquran.cloud edition id, e.g. en.sahih, fr.hamidullah, or "" for Arabic only.
Run tabligh with no arguments in a terminal to open the interactive menu β a self-contained control center for everything:
tabligh # opens the menu (in a TTY)
- Generate a reel β random or pick a passage; renders locally (with live progress), then offers to open the video or publish it.
- Publish now β generate + publish in one step.
- Control panel β Start / Stop / Restart the web panel as a background service, open it in your browser, or tail its logs β no separate process to babysit.
- Queue β add/remove passages the scheduler plays before random picks.
- History & analytics β totals, by-platform breakdown, recent posts.
- Settings β language, background source (incl. local folder), scheduler on/off, schedule, content, channels, and API keys β all applied live.
- Doctor β one-glance health check (ffmpeg, Chrome, keys, storage, disk).
Localized in English / Ψ§ΩΨΉΨ±Ψ¨ΩΨ© / FranΓ§ais (set UI_LANG or switch it in Settings). Non-interactive contexts (pipes, Docker, CI) print the classic help instead, so scripting is unaffected. There's also tabligh menu (force it) and tabligh doctor (run just the health check).
Open the app root (http://localhost:1998) for a password-protected panel:
- First run shows a setup screen to create your password (or run
tabligh initfor a terminal wizard β it now ends by showing your dashboard URL and offering to start the panel). - Dashboard β status, one-click Generate now / + publish, latest preview.
- Generate β pick a passage or go random, preview before it posts.
- Settings β schedule (timezone + time picker), content (translation, ayah count, max length), branding (karaoke, fill color, particles, animated background, outro promo), platform channel ids, API keys & storage β applied live.
- Queue β plan specific passages; the scheduler plays them before random picks.
- History / Analytics β every render + post, totals, by-platform, recent logs.
- Language β switch the panel between English, Ψ§ΩΨΉΨ±Ψ¨ΩΨ© (RTL) and FranΓ§ais.
Login is rate-limited (5 tries β 15-min lockout). Reset the password from the server with
tabligh set-password <new> (e.g. docker exec <container> tabligh set-password β¦).
Settings live in the store and persist on the /app/data volume; .env only seeds them on first run.
The app has no domain of its own β it just listens on a port (1998 by default, override with PORT) on all interfaces. What URL you open depends on where it runs:
| Where it runs | URL you open | HTTPS? |
|---|---|---|
Your own computer (npm / local) |
http://localhost:1998 |
β (local, fine) |
| Cloud VPS, raw port exposed | http://<your-server-ip>:1998 |
β no |
| VPS behind a reverse proxy | https://yourdomain.com |
β proxy provides it |
- Locally, the setup wizard prints the exact link on start (
http://localhost:1998). - On a VPS, reaching
http://<server-ip>:1998also requires your firewall / security group to allow inbound1998. β οΈ Don't leave the raw port exposed to the internet. The panel serves plain HTTP, so your login password would travel unencrypted. Put it behind a reverse proxy that terminates TLS:- Coolify (recommended) β set a domain on the app and point it at port
1998; Coolify's Traefik handles routing and a Let's Encrypt certificate automatically. - Nginx / Caddy β
proxy_pass http://127.0.0.1:1998behind your domain + cert.
- Coolify (recommended) β set a domain on the app and point it at port
The app never needs to know its public domain; the proxy owns the domain and HTTPS and forwards to 1998 internally.
Publishing goes through Buffer, which fans out to every connected platform.
- Create a Buffer account and connect your TikTok / Instagram / Facebook / YouTube channels.
- Set
BUFFER_ACCESS_TOKEN, then runnpm start channelsto list your channel ids. - Put the ids into the matching
BUFFER_*_CHANNEL_IDSvars (any subset β TikTok only is fine). npm start random -- --publish(or let the scheduler do it).
Each platform gets the right format automatically (Reel / Short). The caption includes the surah, ayah range, reciter, photo credit, and hashtags.
Object storage is used only for publishing: the reel is uploaded to an S3 bucket so Buffer's servers can fetch it from a public URL. If you only render locally (no publishing), you need no storage at all.
The MINIO_* settings are just standard S3 credentials β any S3-compatible provider works, not only MinIO:
| Provider | Install? | Notes |
|---|---|---|
| Cloudflare R2 | β | Free tier + public buckets β easiest |
| AWS S3 / Backblaze B2 / Wasabi / DO Spaces | β | Cloud bucket + access keys |
| Self-hosted MinIO | β | Only worth it on a server with a public domain |
MINIO_PUBLIC_URL must be reachable from outside your machine. A MinIO on localhost/your LAN won't work (Buffer can't reach it) β use a cloud bucket, or host MinIO behind a public domain (e.g. on the same box as your panel). The app auto-creates the bucket and sets a public-read policy on first publish, and prunes old objects after MINIO_RETENTION_HOURS.
One command β persistence included, nothing to set up:
cp .env.example .env # fill in your keys (optional β you can also do it in the panel)
docker compose up -d # scheduler + control panel, on http://localhost:1998That's it. On first boot the app creates its own config store at data/store.json
(seeded from your .env) β you never create or "link" anything. The bundled
docker-compose.yml mounts a named volume at /app/data, so your settings, panel
password, queue and history persist across restarts and rebuilds automatically.
serve (the default command) starts:
- a control panel at
/β password-protected; manage settings, generate/preview a reel, post now, browse history/analytics, and queue passages. Change anything live, no redeploy. - an internal scheduler that renders + publishes at every
PUBLISH_TIMESin yourTZ; GET /healthand a token-securedGET /trigger?key=<TRIGGER_TOKEN>for scripting.
Headless mode: run tabligh serve --no-panel (or set PANEL_ENABLED=false) to keep the
scheduler posting while exposing no HTTP surface at all β ideal if you only manage the app
from the terminal (tabligh menu) and don't want a web panel to secure.
Config lives in the store after first boot (so the panel can edit it live). .env only
seeds it once β to change things later, use the panel (or tabligh set-password to reset
the password). Delete data/store.json to re-seed from .env.
Everything above is yours β your keys, your accounts, your schedule. The web studio is the opposite: an anonymous, render-only site anyone can use without installing anything. It is what runs at tabligh.cc.
The SPA lives in web/, in this repo. npm run build:all compiles the CLI and
builds it into dist/web, which tabligh web then serves.
npm run build:all # compiles the CLI and builds the SPA into dist/web
tabligh web # public studio on :1999docker compose up -d brings up both services from the same image β the private panel on
127.0.0.1:1998 and the public studio on :1999.
The public app is a separate process from the panel, and the separation is structural rather than a matter of checking a flag:
- It cannot publish.
publish: falseis hard-coded at the point the job is built. There is no route, parameter or setting that reaches Buffer or your social accounts. - It cannot read or write your settings. In compose,
/app/datais mounted read-only. - It has no session, no login, no cookies β so there is nothing to steal or escalate.
- Every input is validated against a closed list. The reciter must exist in the registry (it becomes part of a URL), the karaoke colour must be a literal hex (it is interpolated into the render page's CSS), and the local-image folder is simply not settable from the web.
A reel is rendered frame by frame β roughly 25 Puppeteer screenshots per second of recitation, then an x264 encode. That is minutes of pinned CPU per video, and it is the real constraint on the whole thing. The defaults keep a small VPS usable:
| Variable | Default | What it controls |
|---|---|---|
WEB_CONCURRENCY |
1 |
Simultaneous renders. Each pins a Chrome + an ffmpeg. |
WEB_MAX_AYAHS |
10 |
Longest passage. The single biggest lever on cost. |
WEB_RATE_PER_HOUR |
5 |
Renders per IP per hour. |
WEB_QUEUE_MAX |
20 |
Refuse new jobs rather than promise a 40-minute wait. |
WEB_JOB_TTL_MINUTES |
60 |
How long a finished MP4 stays downloadable. |
WEB_TRUST_PROXY |
false |
Honour X-Forwarded-For. Only enable behind a proxy you control. |
TURNSTILE_SITE_KEY / TURNSTILE_SECRET |
empty | Optional Cloudflare captcha. |
Nothing is stored: the job lives in memory, the file is swept after the TTL, and no request detail is logged or tracked.
- Ten languages (en Β· ar Β· fr Β· es Β· tr Β· id Β· ms Β· ur Β· fa Β· bn) with full RTL, matching the READMEs and the terminal UI.
- Server-rendered SEO β a Vite SPA ships an empty
<div>, so the Node app rewrites the builtindex.htmlper request: per-locale title/description/canonical,hreflangacross all ten locales, OpenGraph/Twitter cards, JSON-LD (WebApplication,FAQPage,HowTo), plus a crawlable copy of the marketing text on first byte./sitemap.xmland/robots.txtincluded. SITE_URLsets the canonical origin and the address shown in the outro of every reel.
The reciter cards fall back to a monogram. Drop <reciter-id>.jpg (e.g. husary.jpg) into
web/public/reciters/ and it is picked up automatically β no code change. Ids come from
src/quran/reciters.ts.
npm run web # API on :1999
npm run dev:web # Vite on :5273, proxying /api and /d to :1999On Coolify / Railway / Fly: point it at this repo. If you deploy the docker-compose.yml,
the volume is created for you β zero manual steps. If you use the plain Dockerfile, the
VOLUME /app/data line makes most platforms persist it automatically; on Coolify you can also
add one Persistent Storage mounted at /app/data. Set your env vars and deploy.
First run with no CLI? Just open the panel β it shows a setup screen to create your
password. Prefer the terminal? Run tabligh init for an interactive wizard.
- YouTube-source mode (yt-dlp + Whisper) for arbitrary recitations
- True forced-alignment karaoke (word-accurate) via Quran.com segments
- De-dup ledger so passages don't repeat until the Mushaf is cycled
- More layouts / themes
- Recitations: everyayah.com Β· Text & translations: alquran.cloud
- Backgrounds: Pexels / Unsplash (credited in every caption)
- Fonts: Mada (ayah body), Aref Ruqaa (headers), Reem Kufi (outro), Ubuntu (UI) (SIL OFL / UFL)
- Rendering: Puppeteer + ffmpeg
MIT β do good with it. Please keep recitations and Qur'anic text presented respectfully.
If this helps you spread a good word, β the repo and check out @eQurany.
