Add README with full architecture docs
This commit is contained in:
@@ -0,0 +1,138 @@
|
|||||||
|
# Radio Susan 📻
|
||||||
|
|
||||||
|
Personal internet radio station. 24/7 automated playback from a music library with smart DJ logic, energy-based scheduling, TTS announcements, and jingles.
|
||||||
|
|
||||||
|
**Stream:** `https://radio.jihakuz.xyz/stream` (MP3 192kbps)
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
|
||||||
|
| Component | Role |
|
||||||
|
|-----------|------|
|
||||||
|
| **Icecast2** | Streaming server |
|
||||||
|
| **Liquidsoap** | Radio automation — reads playlist, inserts jingles, outputs to Icecast |
|
||||||
|
| **generate_daily_playlist.py** | Builds a full 24h playlist with baked-in TTS announcements |
|
||||||
|
| **generate_site.py** | Generates a static HTML "now playing" site from the playlist |
|
||||||
|
| **radio_dj.py** | Fallback per-track DJ when no daily playlist exists |
|
||||||
|
| **dj_core.py** | Shared logic: track loading, energy filtering, cooldowns, modes |
|
||||||
|
| **analyse_tracks.py** | Essentia audio feature extraction (energy, BPM) |
|
||||||
|
| **build_genre_cache.py** | Genre cache from beets DB |
|
||||||
|
| **generate_jingles.py** | Generates jingle MP3s from TTS + SFX |
|
||||||
|
| **edge-tts** | Microsoft Edge TTS engine for announcements |
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
### Daily Pipeline
|
||||||
|
|
||||||
|
Every midnight, `generate_daily_playlist.py` builds a complete 24-hour playlist:
|
||||||
|
|
||||||
|
1. **Loads all tracks** from beets DB + genre cache + Essentia audio features
|
||||||
|
2. **For each hour**, looks up target energy from an energy curve (ambient at night, high energy in the evening)
|
||||||
|
3. **Picks a mode** (weighted random): shuffle 40%, deep cuts 20%, new to library 15%, old favourites 15%, full album 10%
|
||||||
|
4. **Selects a track** using contrast maximisation — prefers energy distance from recent picks
|
||||||
|
5. **Every 3 tracks**, generates a TTS announcement via edge-tts
|
||||||
|
6. **Outputs** `playlist.json` + `playlist.m3u` for Liquidsoap
|
||||||
|
|
||||||
|
Liquidsoap picks up the new playlist automatically via file watching.
|
||||||
|
|
||||||
|
### Fallback Chain
|
||||||
|
|
||||||
|
If the daily playlist is empty or missing:
|
||||||
|
1. `radio_dj.py` — same smart DJ logic, picks one track at a time
|
||||||
|
2. `all.m3u` — pure random shuffle of the entire library (last resort)
|
||||||
|
|
||||||
|
### Energy Schedule
|
||||||
|
|
||||||
|
The DJ schedules by **energy level**, not genre. Whatever genre fits the energy gets played.
|
||||||
|
|
||||||
|
| Show | Hours | Energy | Vibe |
|
||||||
|
|------|-------|--------|------|
|
||||||
|
| Night Drift | 00–07 | 1 | Ambient, drone |
|
||||||
|
| Morning Calm | 07–09 | 2–3 | Downtempo, chill |
|
||||||
|
| Late Morning | 09–12 | 4–5 | Indie, jazz, trip-hop |
|
||||||
|
| Lunch Break | 12–14 | 4 | Soul, dub |
|
||||||
|
| Afternoon Session | 14–17 | 5–6 | Electronic, funk |
|
||||||
|
| Drive Time | 17–19 | 5–6 | Mixed |
|
||||||
|
| Evening Heat | 19–22 | 7–8 | Techno, house, DnB |
|
||||||
|
| Wind Down | 22–00 | 2–4 | Comedown, ambient |
|
||||||
|
|
||||||
|
### Smart Modes
|
||||||
|
|
||||||
|
| Mode | Weight | Strategy |
|
||||||
|
|------|--------|----------|
|
||||||
|
| shuffle | 40% | Random from energy-matched pool |
|
||||||
|
| deep_cuts | 20% | Tracks with 0 plays |
|
||||||
|
| new_to_library | 15% | Added in last 14 days |
|
||||||
|
| old_favourites | 15% | Highest play count |
|
||||||
|
| full_album | 10% | Plays entire album in order |
|
||||||
|
|
||||||
|
### Cooldowns
|
||||||
|
|
||||||
|
- **Track:** 7 days
|
||||||
|
- **Artist:** 3 hours
|
||||||
|
- **Album:** 24 hours
|
||||||
|
- **Genre:** no back-to-back same genre
|
||||||
|
|
||||||
|
### Announcements vs Jingles
|
||||||
|
|
||||||
|
- **Announcements** are baked into the playlist at generation time — pre-rendered TTS clips inserted into the M3U every 3 tracks.
|
||||||
|
- **Jingles** are dynamic in Liquidsoap — randomly rotated from `/jingles/`, roughly 1 per 4 tracks.
|
||||||
|
|
||||||
|
## Audio Analysis
|
||||||
|
|
||||||
|
`analyse_tracks.py` uses Essentia to extract per-track features:
|
||||||
|
|
||||||
|
- **Energy** — normalised via percentile breakpoints to a 1–10 scale
|
||||||
|
- **BPM** — via RhythmExtractor2013
|
||||||
|
|
||||||
|
Results in `audio_features.db` (SQLite). Incremental — only analyses new files.
|
||||||
|
|
||||||
|
**Known issue:** BPM detection is unreliable for fast genres (hardcore, breakcore, DnB) — often reads half or a third of the real tempo.
|
||||||
|
|
||||||
|
## Static Website
|
||||||
|
|
||||||
|
`generate_site.py` builds a single-page dark-themed site:
|
||||||
|
|
||||||
|
- Now Playing banner (polls track state)
|
||||||
|
- Up Next preview
|
||||||
|
- Stats bar
|
||||||
|
- Collapsible show cards with full tracklists
|
||||||
|
- Inline listen button
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Copy secrets template
|
||||||
|
cp secrets.liq.example secrets.liq
|
||||||
|
# Edit with your Icecast password
|
||||||
|
nano secrets.liq
|
||||||
|
|
||||||
|
# Install Python deps
|
||||||
|
python3 -m venv venv
|
||||||
|
source venv/bin/activate
|
||||||
|
pip install edge-tts essentia
|
||||||
|
|
||||||
|
# Crons (add to crontab)
|
||||||
|
0 0 * * * sg mediaserver "python3 generate_daily_playlist.py"
|
||||||
|
5 0 * * * sg mediaserver "python3 generate_site.py"
|
||||||
|
0 7 * * * python3 build_genre_cache.py
|
||||||
|
30 7 * * * venv/bin/python3 analyse_tracks.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Data Sources
|
||||||
|
|
||||||
|
| Source | Used for |
|
||||||
|
|--------|----------|
|
||||||
|
| Beets DB | Track metadata, genres, added date |
|
||||||
|
| Navidrome DB | Play counts, starred tracks |
|
||||||
|
| audio_features.db | Essentia energy + BPM |
|
||||||
|
| genre_cache.json | Genre-to-energy mapping |
|
||||||
|
|
||||||
|
## SFX Samples
|
||||||
|
|
||||||
|
The `sfx/` directory contains sound effects used by `generate_jingles.py`:
|
||||||
|
|
||||||
|
`airhorn` · `cashregister` · `countdown` · `crash` · `demon` · `meow` · `pirate_arr` · `pirate_plank` · `pirate_yaargh` · `reverb_hit` · `scream` · `static` · `sweep` · `synth_horn` · `text_tone`
|
||||||
|
|
||||||
|
## Resource Usage
|
||||||
|
|
||||||
|
Minimal. Icecast ~10MB RAM, Liquidsoap ~50–70MB RAM. Playlist generation ~2.5 minutes (mostly TTS). Analysis ~1s/track.
|
||||||
Reference in New Issue
Block a user