summaryrefslogtreecommitdiff
path: root/README.md
diff options
context:
space:
mode:
authorCaine <caine@jihakuz.xyz>2026-03-26 22:53:50 +0000
committerCaine <caine@jihakuz.xyz>2026-03-26 22:53:50 +0000
commit2c404234364fe8c86c1ac4de34c3fd1d6453c66d (patch)
treef3922dd72600323ebe63ff873c9246fdfc146c08 /README.md
parent091042cc02a84090b94bb0cd2b38382bbd916aac (diff)
Add README with full architecture docsHEADmaster
Diffstat (limited to 'README.md')
-rw-r--r--README.md138
1 files changed, 138 insertions, 0 deletions
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..e8303b2
--- /dev/null
+++ b/README.md
@@ -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.