SARAL AI is a full-stack application that transforms research papers (PDF, arXiv, BioRxiv, patents) into AI-generated video presentations, podcasts, posters, reels, slides, and business briefs. It leverages a Go gateway orchestrator, seven workers across five services, and a Next.js 16 frontend with Firebase authentication to deliver a seamless pipeline from document upload to downloadable media.
Developed at Precog Labs, IIIT Hyderabad, this national initiative by the Anusandhan National Research Foundation (ANRF) aims to democratize knowledge and improve how research is consumed. Recognized by Indian Ministry of Science and Technology, featured on Nature article, and Analytics India Magazine (AIM).
saral/
├── backend/
│ ├── gateway/ Go REST API + orchestrator (port 8080)
│ │ ├── internal/ auth, pipeline, db, sse, gemini, sarvam, …
│ │ ├── .env ← you create (gateway-only config)
│ │ └── firebase-service-account.json ← you download (secret)
│ ├── services/
│ │ ├── pdf-parser/ Python worker — PDF text/image extraction
│ │ ├── beamer/ Python worker — LaTeX slides + posters (2 processes)
│ │ ├── ffmpeg-job/ Python worker — video stitching
│ │ ├── script-gen/ Go worker — Gemini script generation
│ │ ├── audio-gen/ Go worker — Sarvam/Bhashini TTS
│ │ └── shared/ Python shared lib (saral_shared)
│ ├── migrations/ Plain SQL, applied with psql
│ ├── docker-compose.yml Postgres + Redis + fake-GCS
│ ├── Procfile.dev overmind process definitions
│ ├── .env.shared ← you create (shared by ALL workers)
│ ├── ROUTES.md Full API reference
│ └── ARCHITECTURE.md System design & data flow
└── frontend/
├── app/ Next.js 16 App Router pages
├── components/ ui (shadcn), dashboard, modals, landing, auth
├── lib/ api client, zustand stores, firebase, types
└── .env.local ← you create (Firebase web config)
Architecture & User Flow:
- The Next.js frontend (port 3000) talks to the Go gateway (port 8080).
- The gateway authenticates via Firebase (optional locally), stores state in Postgres, uploads artifacts to GCS (fake-GCS locally), and enqueues jobs on Redis Streams.
- Seven workers consume those streams and report back via webhooks; the gateway pushes progress to the browser over SSE.
flowchart TD
User([User]) -->|Uploads Paper PDF| UI[Next.js Frontend]
UI -->|API POST /upload| Gateway[Go Gateway API]
Gateway -->|Save File| Storage[(Storage: GCS / fake-GCS)]
Gateway -->|Enqueue Task| Redis[Redis Streams]
subgraph Workers
PDF[pdf-parser]
Script[script-gen]
Audio[audio-gen]
Beamer[beamer_compile]
FFmpeg[ffmpeg_stitch]
end
Redis -->|1. Parse| PDF
PDF -->|Webhook| Gateway
Gateway -->|2. Generate| Script
Script -->|Webhook| Gateway
Gateway -.->|3. Pause for human review| UI
UI -->|Confirm Script| Gateway
Gateway -->|4. Parallel Processing| Audio
Gateway -->|4. Parallel Processing| Beamer
Audio -->|Webhook| Gateway
Beamer -->|Webhook| Gateway
Gateway -->|5. Stitch Media| FFmpeg
FFmpeg -->|Webhook| Gateway
Gateway -->|SSE Progress| UI
UI -->|Download video.mp4| User
For the deeper architecture guide, see backend/ARCHITECTURE.md.
- Repository Layout
- 1. Prerequisites
- 2. Backend Setup
- 3. Frontend Setup
- 4. Daily Workflow
- 5. Ports Reference
- 6. Troubleshooting
- 7. Environment Variable Reference
- 8. Further Reading
- 9. Notes for Contributors
macOS:
- Ensure you have Homebrew installed.
Windows (WSL2):
You must use WSL2 (Windows Subsystem for Linux). Do not try to run
overmindorpoppleron native Windows.
- Open PowerShell as Administrator and run
wsl --install. - Open your new
Ubuntuterminal and install Homebrew for Linux:
# 1. Install Homebrew for Linux
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# 2. Add Homebrew to your PATH
(echo; echo 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"') >> ~/.bashrc
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"
# 3. Install GCC (required by Homebrew on Linux)
sudo apt-get update && sudo apt-get install -y build-essentialLinux (Ubuntu/Debian): Follow the same Homebrew installation block as WSL2 above, OR use your native package manager for the dependencies below.
Once Homebrew is available (macOS, Linux, or WSL2):
# Core toolchain
brew install go python@3.11 uv overmind redis postgresql node
# Worker system dependencies
brew install ffmpeg poppler # ffmpeg-job + beamer (pdf2image)macOS:
# Option A (full distribution):
brew install --cask mactex-no-gui
sudo tlmgr update --self
sudo tlmgr install beamer latexmk
# Option B (smaller install):
brew install --cask basictex
export PATH="/Library/TeX/texbin:$PATH"
sudo tlmgr update --self
sudo tlmgr install beamer collection-latexrecommended collection-fontsrecommended xetex latexmkWindows (WSL2) & Linux:
sudo apt-get install -y texlive-xetex texlive-fonts-recommended texlive-latex-recommendedInstall Docker Desktop (enable WSL2 integration on Windows).
Verify everything is ready:
go version # ≥ 1.25
python3.11 --version # 3.11.x
docker compose version
overmind --version
xelatex --version # after terminal restart
ffmpeg -version
node --version # ≥ 20| Key | Where to get it | Used by |
|---|---|---|
| Gemini API key | https://aistudio.google.com/app/apikey | script-gen, audio-gen, gateway |
| OpenRouter API key | https://openrouter.ai/keys | script-gen (when LLM_PROVIDER=openrouter) |
| Sarvam API key | https://dashboard.sarvam.ai | audio-gen (TTS) |
| LinkedIn API keys | https://www.linkedin.com/developers/apps — create an app, request 'Share on LinkedIn' and 'Sign In with LinkedIn' | gateway (social publishing) |
| YouTube API keys | https://console.cloud.google.com/apis/credentials — create OAuth 2.0 Client IDs, enable YouTube Data API v3 | gateway (social publishing) |
All commands from saral/backend/.
docker compose up -d
docker compose ps # wait for postgres + redis → "healthy"Docker Permission Denied Error (WSL/Linux)? If you get
permission denied while trying to connect to the docker API at unix:///var/run/docker.sock, you can either run the commands withsudo(e.g.sudo docker compose up -d), or permanently fix it by adding your user to the docker group:sudo usermod -aG docker $USER newgrp docker
This starts (host ports):
| Container | Host port | Credentials / notes |
|---|---|---|
| postgres:15-alpine | 5433 | user saral_app, password localpassword, db saral. init.sql auto-applies on first boot. |
| redis:7-alpine | 6380 | job streams |
| fake-gcs-server | 4443 | bucket saral-artifacts-local auto-created by the fake-gcs-init container |
The DB password is
localpassword(defined in docker-compose.yml). The oldbackend/SETUP.mdsayssaral_pass— that is wrong.
Health checks:
psql "postgresql://saral_app:localpassword@localhost:5433/saral" -c "SELECT 1;"
redis-cli -p 6380 ping # PONG
curl -s http://localhost:4443/storage/v1/b | head -5 # JSONinit.sql is applied automatically by the container. The numbered ones are not — apply manually, in order:
for f in migrations/0*.sql; do
echo "== $f"
psql "postgresql://saral_app:localpassword@localhost:5433/saral" -f "$f"
doneVerify: psql "postgresql://saral_app:localpassword@localhost:5433/saral" -c "\dt" → should list users, papers, pipeline_runs, pipeline_steps, artifacts, plus podcast/reel/slides/social/patent tables.
Loaded by all six workers (Python workers via load_dotenv(../../.env.shared), Go workers via godotenv.Load("../../.env.shared")). The gateway does not read this file.
# ── Infrastructure (matches docker-compose.yml) ─────────────────
ENV=local
DATABASE_URL=postgresql://saral_app:localpassword@localhost:5433/saral
REDIS_URL=redis://localhost:6380
STORAGE_BUCKET=saral-artifacts-local
STORAGE_EMULATOR_HOST=http://localhost:4443
# ── Worker → gateway callbacks ──────────────────────────────────
GATEWAY_WEBHOOK_URL=http://localhost:8080
FRONTEND_BASE_URL=http://localhost:3000
# ── External APIs (REQUIRED — pipeline dies without these) ─────
GEMINI_API_KEY=<your-gemini-key> # script_gen step fails if missing
SARVAM_API_KEY=<your-sarvam-key> # audio_gen step fails if missing
GCP_PROJECT_ID=<your-gcp-project-id>
# ── Translation (sarvam is the local-dev default) ──────────────
TRANSLATION_PROVIDER=sarvam
# ── Social publishing (optional — leave blank locally) ─────────
LINKEDIN_CLIENT_ID=
LINKEDIN_CLIENT_SECRET=
LINKEDIN_REDIRECT_URI=The script-gen worker is LLM-agnostic. Pick a backend via LLM_PROVIDER in backend/.env.shared:
| Mode | When to use | Required env vars |
|---|---|---|
vertex (default) |
Existing GCP deployments; uses Vertex AI Gemini | GCP_PROJECT_ID, ADC |
gemini_api |
Quick Gemini access via direct API key | GEMINI_API_KEY |
openrouter |
Any model on OpenRouter (free tier, GPT, Claude, Llama, etc.) | OPENROUTER_API_KEY (+ optional model overrides) |
Example openrouter block (uncomment in .env.shared and fill in your own key):
LLM_PROVIDER=openrouter
OPENROUTER_API_KEY=sk-or-v1-...
OPENROUTER_FLASH_MODEL=openrouter/free
OPENROUTER_PRO_MODEL=openai/gpt-oss-120b:free
OPENROUTER_SITE_URL=http://localhost:3000
OPENROUTER_SITE_NAME="Saral Dev"Any OpenRouter model slug works for OPENROUTER_FLASH_MODEL and OPENROUTER_PRO_MODEL. The HTTP-Referer (OPENROUTER_SITE_URL) and X-Title (OPENROUTER_SITE_NAME) headers are recommended by OpenRouter for rankings and attribution.
Loaded only by the gateway (its working directory is gateway/, so relative paths resolve from there):
ENV=local
PORT=8080
DATABASE_URL=postgresql://saral_app:localpassword@localhost:5433/saral
REDIS_URL=redis://localhost:6380
STORAGE_BUCKET=saral-artifacts-local
STORAGE_EMULATOR_HOST=http://localhost:4443
FRONTEND_BASE_URL=http://localhost:3000
# ── GCP (prod only — blank locally) ────────────────────────────
GCP_PROJECT_ID=
GCP_REGION=asia-south1
# ── Per-user API key encryption (users table stores encrypted
# gemini/sarvam keys) — generate: openssl rand -base64 32 ────
KEYS_ENCRYPTION_KEY=<random-32-byte-base64>
# ── Optional social integrations — blank locally ───────────────
SARVAM_API_KEY=<your-sarvam-key>
YOUTUBE_CLIENT_ID=
YOUTUBE_CLIENT_SECRET=
YOUTUBE_REDIRECT_URI=| File | What | How |
|---|---|---|
gateway/internal/auth/common_passwords.txt |
SecLists 10k list, embedded at compile time | If missing: curl -s https://raw.githubusercontent.com/danielmiessler/SecLists/master/Passwords/Common-Credentials/10k-most-common.txt -o gateway/internal/auth/common_passwords.txt |
services/audio-gen/models.json |
Bhashini model registry | A stub file containing just [] is fine when TRANSLATION_PROVIDER=sarvam. Real Bhashini credentials only needed for the 10 extended Indic languages. |
# Go gateway
cd gateway && go mod tidy && cd ..
# Python workers — ONLY these four are Python (script-gen & audio-gen are Go)
# UV_LINK_MODE=copy prevents permission errors on WSL/Windows cross-filesystem mounts
export UV_LINK_MODE=copy
for svc in pdf-parser beamer ffmpeg-job shared; do
cd services/$svc
uv sync --python 3.11
cd ../..
doneThe Go workers (script-gen, audio-gen) need no install step — go run resolves modules on first start.
overmind start -f Procfile.dev** WSL Error:
bind: operation not supported?** Overmind uses Unix sockets, which Windows file systems (/mnt/c/) do not natively support. You must tell Overmind to place its socket file on the Linux side (like/tmp/):OVERMIND_SOCKET=/tmp/overmind.sock overmind start -f Procfile.dev
Seven processes start: gateway, pdf-parser, latex-worker, poster-worker, ffmpeg-worker, script-gen, audio-gen.
Overmind kills all processes if any one crashes. When reading crash logs, find the first
Exited with code 1— everything after it showingKeyboardInterrupt/signal: interruptis collateral, not a real error.
curl http://localhost:8080/health
# → {"success":true,"data":{"status":"ok"},...}
# Upload a test paper using the local auth bypass (no Firebase needed):
curl -X POST http://localhost:8080/api/papertovideo/upload \
-H "X-User-ID: testuser1" \
-F "pdf=@/path/to/paper.pdf"
# Watch it process live:
curl -N http://localhost:8080/api/papertovideo/<run_id>/stream -H "X-User-ID: testuser1"The pipeline pauses after script_gen for human review — confirm to continue:
curl -X POST http://localhost:8080/api/papertovideo/<run_id>/script/confirm \
-H "X-User-ID: testuser1" -H "Content-Type: application/json" -d '{}'Full endpoint walkthrough: backend/ROUTES.md.
All commands from saral/frontend/.
Values from Firebase Console → Project Settings → General → Your apps → Web app (create a web app if none exists):
NEXT_PUBLIC_FIREBASE_API_KEY=<apiKey>
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=<project>.firebaseapp.com
NEXT_PUBLIC_FIREBASE_PROJECT_ID=<projectId>
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=<project>.appspot.com
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=<senderId>
NEXT_PUBLIC_FIREBASE_APP_ID=<appId>
# Gateway URL (this is also the default if unset)
NEXT_PUBLIC_GATEWAY=http://localhost:8080NEXT_PUBLIC_* vars are inlined at build time — restart the dev server after changing them.
bun install # postinstall copies pdfjs worker → public/pdfjs/
bun run dev # → http://localhost:3000| Layer | Choice |
|---|---|
| Framework | Next.js 16 App Router, React 19, TypeScript |
| Styling | Tailwind v4, tokens in app/globals.css @theme inline |
| Components | shadcn (components/ui/) — add via npx shadcn@latest add <name> |
| State | zustand: lib/auth-store.ts, lib/paper-store.ts, lib/artifact-store.ts |
| API client | lib/api.ts (all gateway calls), types in lib/types.ts |
| Auth | Firebase client SDK (lib/firebase.ts); guard in app/dashboard/layout.tsx |
| Theming | next-themes via context/ThemeContext.tsx (light default, class-based dark) |
# Terminal 1 — infra (once; survives reboots until `docker compose down`)
cd saral/backend && docker compose up -d
# Terminal 2 — backend services
cd saral/backend && overmind start -f Procfile.dev
# Terminal 3 — frontend
cd saral/frontend && bun run dev| Task | Command |
|---|---|
| Logs for one service | overmind connect gateway (Ctrl+B, D to detach) |
| Restart one service | overmind restart latex-worker |
| Stop backend services | overmind kill |
| Stop infra (keep data) | docker compose down |
| Stop infra (wipe data) | docker compose down -v (numbered migrations must be re-applied after) |
| Service | Port | What |
|---|---|---|
| Frontend (Next.js) | 3000 | Web UI |
| Gateway (Go/Gin) | 8080 | REST API + SSE |
| PostgreSQL | 5433 | State (container 5432) |
| Redis | 6380 | Job streams (container 6379) |
| fake-GCS | 4443 | Artifact storage emulator |
| Error | Fix |
|---|---|
pattern common_passwords.txt: no matching files found |
Download SecLists file → gateway/internal/auth/common_passwords.txt (see §2.5) |
bhashini registry: read models.json: no such file |
Create services/audio-gen/models.json with [] (see §2.5) |
Firebase init failed: cannot read credentials file |
Service-account JSON missing at path in FIREBASE_CREDENTIALS_FILE (relative to gateway/) |
Firebase init failed: no private key data found |
The JSON is a placeholder — download the real key from Firebase Console |
dial tcp [::1]:6380: connect: connection refused |
Infra not running or ports unbound: docker compose ps must show 0.0.0.0:6380->6379. If not: docker compose down && docker compose up -d from backend/ |
Python ModuleNotFoundError |
Recreate that worker's venv (§2.6). Note: only 4 services are Python — running uv sync in script-gen/audio-gen errors with "No pyproject.toml" (harmless) |
xelatex failed / blank slides |
brew install --cask basictex, restart terminal |
| ffmpeg-worker fails to stitch | brew install ffmpeg |
| pdf2image / poppler errors | brew install poppler |
Check error_message: curl -s localhost:8080/api/papertovideo/<run_id>/status -H "X-User-ID: testuser1"
| Stage | Usual cause |
|---|---|
script_gen |
GEMINI_API_KEY missing/invalid in backend/.env.shared, or (when LLM_PROVIDER=openrouter) OPENROUTER_API_KEY missing/invalid (OPENROUTER_API_KEY is required when LLM_PROVIDER=openrouter) |
audio_gen |
SARVAM_API_KEY missing/invalid in backend/.env.shared |
beamer_compile |
xelatex missing, or font issues (check overmind connect latex-worker) |
ffmpeg_stitch |
ffmpeg missing |
| Pipeline "stuck" after script_gen | Not stuck — it's the intentional human-review pause. POST /script/confirm. |
| Symptom | Fix |
|---|---|
auth/invalid-api-key in browser console |
.env.local Firebase values missing/wrong; restart dev server after editing |
| API calls hit wrong host | Set NEXT_PUBLIC_GATEWAY=http://localhost:8080 and restart |
| PDF previews broken | public/pdfjs/pdf.worker.min.mjs missing — re-run npm install (postinstall copies it) |
| Login works but dashboard kicks you out | Backend gateway not running, or FIREBASE_PROJECT_ID mismatch between gateway .env and frontend .env.local |
redis-cli -p 6380 XLEN saral:jobs:pdf # queue depth
redis-cli -p 6380 XINFO GROUPS saral:jobs:pdf # consumer lag
redis-cli -p 6380 XRANGE saral:dlq - + COUNT 10 # dead letters| Variable | Required | Purpose |
|---|---|---|
ENV |
yes | local / production |
DATABASE_URL |
yes | Postgres DSN (workers write step state) |
REDIS_URL |
yes | Job streams |
STORAGE_BUCKET |
yes | saral-artifacts-local locally |
STORAGE_EMULATOR_HOST |
local only | http://localhost:4443 — unset in prod |
GATEWAY_WEBHOOK_URL |
yes | Where workers POST completion webhooks |
LLM_PROVIDER |
no | LLM backend: vertex (default), gemini_api, or openrouter |
GEMINI_API_KEY |
depends | Required when LLM_PROVIDER=gemini_api; also used by audio-gen |
OPENROUTER_API_KEY |
when LLM_PROVIDER=openrouter |
OpenRouter bearer token |
OPENROUTER_FLASH_MODEL |
no | OpenRouter model slug for Flash tier; default google/gemini-2.5-flash |
OPENROUTER_PRO_MODEL |
no | OpenRouter model slug for Pro tier; default google/gemini-2.5-pro |
OPENROUTER_SITE_URL |
no | HTTP-Referer header sent to OpenRouter |
OPENROUTER_SITE_NAME |
no | X-Title header sent to OpenRouter |
SARVAM_API_KEY |
yes | audio-gen TTS |
GCP_PROJECT_ID |
yes | Gemini/Vertex project |
TRANSLATION_PROVIDER |
no | sarvam (default) / bhashini |
MODELS_JSON_PATH |
no | Override path to audio-gen models.json |
FRONTEND_BASE_URL |
no | Used in links/redirects |
LINKEDIN_* |
no | Social publishing (blank locally) |
| Variable | Required | Purpose |
|---|---|---|
ENV, PORT |
yes | local, 8080 |
DATABASE_URL, REDIS_URL |
yes | Same values as .env.shared |
STORAGE_BUCKET, STORAGE_EMULATOR_HOST |
yes | Artifact storage |
FIREBASE_PROJECT_ID |
yes | Token verification |
FIREBASE_CREDENTIALS_FILE |
yes | Path to service-account JSON (relative to gateway/) |
FIREBASE_WEB_API_KEY |
for real auth | Email/password + Google OAuth REST flows |
KEYS_ENCRYPTION_KEY |
yes | Encrypts per-user API keys stored in users table |
GEMINI_MODEL |
no | Override default Gemini model |
SARAL_ADMIN_UIDS |
no | Comma-separated admin Firebase UIDs |
GCP_PROJECT_ID, GCP_REGION |
prod only | Cloud Run deployment |
YOUTUBE_*, LINKEDIN_*, DASHBOARD_WEBHOOK_* |
no | Integrations (blank locally) |
| Variable | Required | Purpose |
|---|---|---|
NEXT_PUBLIC_FIREBASE_API_KEY |
yes | Firebase web SDK |
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN |
yes | 〃 |
NEXT_PUBLIC_FIREBASE_PROJECT_ID |
yes | 〃 (must match gateway's FIREBASE_PROJECT_ID) |
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET |
yes | 〃 |
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID |
yes | 〃 |
NEXT_PUBLIC_FIREBASE_APP_ID |
yes | 〃 |
NEXT_PUBLIC_GATEWAY |
no | Gateway URL; defaults to http://localhost:8080 (NEXT_PUBLIC_API_URL also accepted) |
- backend/ARCHITECTURE.md — system diagrams, data flow, scaling
- backend/AUTH_ENDPOINTS.md — auth contract details
- backend/Saral Overhaul API Collection/ — importable API collection
- Never commit secrets. Keep
.env*,firebase-service-account.json, and*firebase*.jsonout of version control — they are already in.gitignore. - Document new environment variables in both
.env.shared/.env.localand in this README immediately. - Update this guide if you add a new local dependency or setup step.
- Run linters before pushing:
go vet ./...,ruff check .,bun run lint,npx tsc --noEmit. - For full contribution guidelines, code of conduct, and PR checklist, see CONTRIBUTING.md.