A content-based recommender for 625 top RadioJavan songs. Pick a song and it returns the 10 most similar tracks, using the song's audio features, popularity and its Persian lyrics.
You can use it in three ways:
- 🌐 Web app: a React UI with live search, backed by a FastAPI REST API
- 🔌 REST API: JSON endpoints with Swagger docs
- 💻 CLI:
python RJ_Recommendation_System.py "Gentleman"
- How it works
- Quick start (Docker Compose)
- Local development
- API reference
- CLI
- Testing
- Performance testing (RPS, p50, p95)
- CI/CD
- Retraining the model
- Project structure
flowchart LR
CSV[RadioJavan_Top.csv<br/>625 songs] --> NUM[14 numeric features<br/>plays, likes, popularity,<br/>danceability, energy, tempo …]
CSV --> LYR[Persian lyrics]
LYR --> PRE[normalize → tokenize →<br/>remove stopwords → lemmatize<br/>hazm]
PRE --> TFIDF[TF-IDF vectors]
NUM --> SCALE[Min-Max scaling]
TFIDF --> SCALE
SCALE --> COS[Cosine similarity<br/>625 × 625 matrix]
COS --> PKL[(model.pkl)]
- Features: RadioJavan plays and likes, Spotify popularity, and audio features (danceability, energy, key, loudness, mode, speechiness, acousticness, instrumentalness, liveness, valence, tempo).
- Lyrics: Persian text is normalized, tokenized, stripped of stopwords and lemmatized with hazm, then turned into TF-IDF vectors.
- Both parts are Min-Max scaled and concatenated. A cosine-similarity matrix between all songs is precomputed and saved as
model.pkl. - At request time, recommending is just sorting one row of that matrix, so responses are fast.
Songs are addressed by
id(their row in the CSV), because some titles belong to more than one artist (for example "Darya" by Masih and by Ehaam).
git clone https://github.com/parvvaresh/Music-recommender-system
cd Music-recommender-system
docker compose up -d --build| Service | URL | Description |
|---|---|---|
| Frontend | http://localhost:3000 | React app served by nginx. /api is proxied to the backend |
| Backend | http://localhost:8000/docs | FastAPI with Swagger UI |
Ports and workers can be changed with environment variables:
FRONTEND_PORT=3010 BACKEND_PORT=8010 WEB_CONCURRENCY=4 docker compose up -d --buildStop it with docker compose down.
Requirements: Python 3.10+ and Node.js 20+.
# Backend
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
uvicorn api.main:app --reload # http://localhost:8000
# Frontend (in another terminal)
cd frontend
npm install
npm run dev # http://localhost:5173The Vite dev server proxies /api to http://localhost:8000. If your API runs somewhere else, set API_PROXY_TARGET:
API_PROXY_TARGET=http://localhost:8001 npm run dev| Variable | Default | Used by |
|---|---|---|
RJ_CSV_PATH |
api/models/data/RadioJavan_Top.csv |
API: song catalogue |
RJ_MODEL_PATH |
api/models/fainal_model/model.pkl |
API: similarity matrix |
RJ_CORS_ORIGINS |
http://localhost:5173 |
API: comma-separated allowed origins |
WEB_CONCURRENCY |
2 |
Backend container: number of uvicorn workers |
API_PROXY_TARGET |
http://localhost:8000 |
Vite dev server: where /api is proxied |
VITE_API_URL |
(same origin) | Frontend build: API base URL |
Interactive docs are available at /docs (Swagger) and /redoc.
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/health |
Health check and number of songs |
| GET | /api/songs?q=&limit=10 |
Search by title or artist (title prefix matches rank first) |
| GET | /api/songs/{id} |
One song |
| GET | /api/songs/{id}/recommendations?k=10 |
Top-k similar songs for a song id |
| GET | /api/recommendations?title=&k=10 |
Same, by title (exact, case-insensitive, or closest fuzzy match) |
limit and k must be between 1 and 50. Unknown songs return 404.
Example
curl "http://localhost:8000/api/recommendations?title=gentleman&k=2"{
"song": { "id": 0, "title": "Gentleman", "artist": "Sasy", "plays": 97011366, "likes": 92837, "popularity": 50, "published": "Mar 6 2019" },
"recommendations": [
{ "id": 229, "title": "Nakoni Bavar", "artist": "Zedbazi", "plays": 20664029, "likes": 27273, "popularity": 38, "published": "Oct 8 2014", "score": 0.3178 },
{ "id": 62, "title": "Mr. Lodeh", "artist": "Amir Tataloo", "plays": 33945088, "likes": 36679, "popularity": 43, "published": "Apr 25 2015", "score": 0.2819 }
]
}python RJ_Recommendation_System.py "Gentleman" rank musicName artistName
1 Nakoni Bavar Zedbazi
2 Mr. Lodeh Amir Tataloo
3 To Ke Nisti Pisham Masih
4 Moohat Mohsen Yeganeh
5 Doctor Sasy
6 Harjaye Shahr Ali Yasini
7 Tekoon Bede Arash
8 Ashegham Kardi Hoorosh Band
9 Ey Vay Sahar
10 Ey Joonam Sami Beigi
If the title isn't an exact match, the closest title is used. The CLI also runs in Docker:
docker build -t radiojavan-cli .
docker run --rm radiojavan-cli "Gentleman"pytest -v # CLI, API and load-test helper tests
cd frontend && npm test # React tests (Vitest + Testing Library)
flake8 . # Lint- API tests (
tests/test_api.py) use a small fake catalogue, plus one check that the real CSV and model are aligned. - Frontend tests (
frontend/src/App.test.jsx) mockfetchand cover search, selecting a song, browsing recommendations, and error states.
perf/load_test.py runs a fixed-duration load test with N concurrent workers. It sends a realistic mix of requests: 40% search, 40% recommend by id and 20% recommend by title. It reports RPS plus p50 / p95 / p99 latency per endpoint and overall.
docker compose up -d --build
python -m perf.load_test --url http://localhost:3000 --duration 30 --concurrency 20Example result (MacBook, through nginx, 2 uvicorn workers):
| Endpoint | Requests | RPS | p50 (ms) | p95 (ms) | p99 (ms) | Errors |
|---|---|---|---|---|---|---|
| recommend_by_id | 1538 | 101.7 | 49.9 | 233.5 | 492.7 | 0 |
| recommend_by_title | 709 | 46.9 | 47.6 | 230.4 | 503.0 | 0 |
| search | 1525 | 100.8 | 52.0 | 237.4 | 412.9 | 0 |
| overall | 3772 | 249.4 | 50.4 | 234.4 | 488.1 | 0 |
Useful flags:
| Flag | Description |
|---|---|
--duration, --concurrency, --warmup |
Load shape (warm-up requests are not measured) |
--max-p95-ms, --max-error-rate, --min-rps |
Exit with code 1 if a threshold is missed |
--json-out FILE |
Save the full report as JSON |
--summary-out FILE |
Append a Markdown table (defaults to $GITHUB_STEP_SUMMARY) |
.github/workflows/ci.yml runs on every push and pull request to main:
| Job | What it does |
|---|---|
build-test-lint |
flake8 and pytest (CLI, API, load-test helpers) |
frontend |
npm ci, Vitest, production build |
performance |
Builds and starts the stack with docker compose, then runs a 30s load test through nginx. Fails if p95 > 1000 ms or error rate > 1%. The RPS/p50/p95/p99 table appears in the run's summary, and the JSON report is uploaded as an artifact |
The training scripts live in api/models/ and need hazm for Persian text processing:
pip install hazm
cd api/models
python save_model.py # writes fainal_model/model.pklRetrain whenever you change the CSV. The API checks on startup that the matrix size matches the number of songs.
.
├── api/
│ ├── main.py # FastAPI app and routes
│ ├── recommender.py # Search, title matching, recommendations
│ ├── Dockerfile # Backend image
│ ├── requirements.txt # Backend runtime dependencies
│ └── models/
│ ├── data/RadioJavan_Top.csv
│ ├── fainal_model/model.pkl # Precomputed cosine-similarity matrix
│ ├── save_model.py # Training entry point
│ ├── pre_preocess.py, model.py, standardize.py
│ └── word2vec/ # Persian text preprocessing and TF-IDF
├── frontend/
│ ├── src/ # React app (Vite) and tests
│ ├── Dockerfile # Multi-stage build → nginx
│ └── nginx.conf.template # Static files and /api reverse proxy
├── perf/load_test.py # Load test (RPS, p50, p95, p99)
├── tests/ # API and load-test helper tests
├── RJ_Recommendation_System.py # CLI
├── test_radiojavan_cli.py # CLI test
├── docker-compose.yml
└── Dockerfile # CLI image