2026-06-07 14:15:42 +01:00
# Live Ozan Radio
2026-06-07 14:47:47 +01:00
Personal AI radio — **no catalog music, ever ** . DeepSeek is the DJ. Google **Lyria 3 ** composes every track. Taste comes from `settings.json` + `taste_seeds.json` — usually built from **Spotify screenshots in Cursor ** (no Spotify API).
2026-06-07 14:15:42 +01:00
Inspired by [Magenta RealTime 2 ](https://magenta.withgoogle.com/magenta-realtime-2 ) (live, ~200ms) and [Lyria 3 ](https://deepmind.google/models/lyria/ ) (full songs via Gemini API). On Mac you can layer MRT2 for true live improvisation; this repo ships the cross-platform Lyria + DeepSeek stack first.
## Stack
| Layer | Product | Role |
|-------|---------|------|
2026-06-07 14:22:39 +01:00
| DJ brain | DeepSeek (Tinqs proxy or BYOK) | Mood, prompts, chat, variety |
2026-06-07 14:15:42 +01:00
| Music engine | Google Lyria 3 Pro / Clip | Generate MP3 tracks |
2026-06-07 14:47:47 +01:00
| Taste | `settings.json` + `taste_seeds.json` | Profile from screenshots or manual edit — see below |
2026-06-07 14:22:39 +01:00
| Player | FastAPI + `gateway/player.html` | Stream queue, library, chat, dashboard |
2026-06-07 14:15:42 +01:00
| Live (optional) | Magenta RealTime 2 | Apple Silicon only — see below |
2026-06-07 14:22:39 +01:00
## Player dashboard
Open **http://127.0.0.1:8787/player ** after starting the server.
| Feature | What it does |
|---------|----------------|
| **Cost dashboard ** | Today's estimated spend, per-track cost, songs generated vs daily cap |
| **Settings (gear icon) ** | Shuffle mode, mix saved + new tracks, new-song chance, max songs/day |
| **Shuffle mode ** | On track end or Skip, picks a random saved song or composes a new one |
| **Saved songs ** | Click any track in the library to play |
| **DJ chat ** | Talk to DeepSeek — requests can trigger new Lyria generations |
### Shuffle behaviour
When shuffle is on (default):
1. If you have saved songs **and ** daily quota remains, each next track has a `new_song_chance` (default 35%) of being freshly composed.
2. Otherwise a random saved song plays (never the same track twice in a row if alternatives exist).
3. If the library is empty, it generates until the daily cap is hit.
Daily generation stats live in `songs/stats.json` (gitignored, local runtime only).
2026-06-07 14:47:47 +01:00
## Taste from Spotify screenshots (recommended)
**No Spotify API keys. ** Screenshot your library (Home, Daily Mixes, playlists), open this repo in **Cursor ** , attach images, and paste the prompt from * * [docs/TASTE-FROM-SCREENSHOTS.md ](docs/TASTE-FROM-SCREENSHOTS.md )**. The agent updates `settings.json` and `taste_seeds.json` .
Share that doc with friends — same flow for group taste (WhatsApp links + screenshots).
2026-06-07 14:19:38 +01:00
## Taste (`settings.json`)
2026-06-07 14:47:47 +01:00
Edit `settings.json` at the repo root, or let Cursor fill it from screenshots. The DJ reloads it on every generate and chat.
2026-06-07 14:22:39 +01:00
``` json
{
"taste" : { "summary" : "..." , "genres" : [ ] , "mood" : [ ] , "instruments" : [ ] } ,
"playback" : {
"shuffle" : true ,
"mix_existing_and_new" : true ,
"new_song_chance" : 0.35
} ,
"limits" : { "max_new_songs_per_day" : 10 } ,
"costs" : {
"lyria_pro_usd" : 0.08 ,
"lyria_clip_usd" : 0.04 ,
"deepseek_per_track_usd" : 0.002
}
}
```
2026-06-07 14:47:47 +01:00
**Example profile (Ozan): ** checked-in [`settings.json` ](settings.json ) + [`taste_seeds.json` ](taste_seeds.json ) — Anadolu psych + ethnic dubtronica from Spotify screenshots. Copy the structure for your own taste.
2026-06-07 14:22:39 +01:00
The player settings panel PATCHes `playback` and `limits` via `/api/settings` and writes back to this file.
2026-06-07 14:19:38 +01:00
2026-06-07 14:18:17 +01:00
## Saved songs
Every track is written to `./songs/` and **committed via Git LFS ** (audio) + plain git (metadata):
| File | Storage | Contents |
|------|---------|----------|
| `{id}_{title}.mp3` | LFS | Audio |
| `{id}.meta.json` | git | Title, mood, DJ line, prompt, lyrics, timestamp |
2026-06-07 14:22:39 +01:00
| `manifest.json` | gitignored | Runtime queue index |
2026-06-07 14:18:17 +01:00
Browse in the player under **Saved songs ** , or `GET /api/songs` . After clone: `git lfs install` then `git lfs pull` .
2026-06-07 14:15:42 +01:00
## Quick start (Forge / Windows)
``` powershell
cd live-ozan -radio
python -m venv . venv
. venv \ Scripts \ activate
pip install -e .
copy . env . example . env
2026-06-07 14:47:47 +01:00
# Fill GEMINI_API_KEY + DEEPSEEK_API_KEY
# Taste: docs/TASTE-FROM-SCREENSHOTS.md (Cursor + Spotify screenshots)
2026-06-07 14:15:42 +01:00
2026-06-07 14:22:39 +01:00
$env:DEEPSEEK_BASE_URL = " https://api.deepseek.com/v1 "
2026-06-07 14:15:42 +01:00
python -m ozan_radio serve
# Open http://127.0.0.1:8787/player
```
One-shot track (no server):
``` powershell
python -m ozan_radio generate
```
## Environment
| Variable | Required | Notes |
|----------|----------|-------|
| `GEMINI_API_KEY` | Yes | [Google AI Studio ](https://aistudio.google.com/apikey ) — Lyria 3 |
2026-06-07 14:22:39 +01:00
| `DEEPSEEK_API_KEY` | Yes | Tinqs proxy token or DeepSeek direct |
| `DEEPSEEK_BASE_URL` | No | Default `https://api.deepseek.com/v1` |
2026-06-07 14:15:42 +01:00
| `LYRIA_MODEL` | No | `lyria-3-pro-preview` (default) or `lyria-3-clip-preview` |
2026-06-07 14:22:39 +01:00
| `RADIO_OUTPUT_DIR` | No | Default `./songs` |
### Cost estimates (defaults)
| Model | ~USD / track |
|-------|----------------|
| Lyria 3 Pro + DeepSeek | ~$0.082 |
| Lyria 3 Clip | ~$0.04 |
At the default cap of 10 new songs/day with Lyria Pro, projected max spend is * * ~$0.82/day**. Adjust `costs` and `limits` in `settings.json` or the player settings panel.
2026-06-07 14:15:42 +01:00
## API
| Method | Path | Description |
|--------|------|-------------|
| GET | `/api/now` | Current track metadata |
| GET | `/api/queue` | Full queue |
2026-06-07 14:22:39 +01:00
| GET | `/api/stats` | Dashboard: today's spend, quota, cost estimates |
| GET | `/api/settings` | Playback, limits, costs |
| PATCH | `/api/settings` | Update shuffle, daily cap, etc. |
2026-06-07 14:15:42 +01:00
| POST | `/api/generate` | DJ plans + Lyria renders next track |
2026-06-07 14:22:39 +01:00
| POST | `/api/shuffle/next` | Smart next: library shuffle or new generation |
| POST | `/api/skip` | Advance (uses shuffle when enabled) |
| GET/POST | `/api/chat` | DJ chat log and messages |
| GET | `/api/songs` | Saved library |
| POST | `/api/songs/{id}/play` | Play a saved track |
2026-06-07 14:15:42 +01:00
| GET | `/stream/{file}` | MP3 stream |
2026-06-07 16:38:52 +01:00
| GET | `/player` | Full DJ dashboard |
| GET | `/winamp` | Winamp-style HTML player (streams `/stream/{file}` locally) |
## Winamp player (pure HTML)
Lightweight **Winamp 2.x– style ** skin — no React, no API required for playback. Shuffles saved songs from the embedded playlist.
| Where | URL |
|-------|-----|
| **Local ** (with server) | **http://127.0.0.1:8787/winamp ** — streams via `/stream/{file}` |
| **Public ** (tinqs.com LFS) | **https://tinqs.com/tinqs/live-radio/src/branch/main/gateway/winamp.html ** |
Controls: play/pause, stop, prev/next, shuffle, volume, clickable playlist. Same `/*__PLAYLIST__*/` embed as `gateway/index.html` — updated by `python -m ozan_radio export-web` or automatically when the server saves a track.
Full dashboard (`/player` ) still needed for DJ chat, Lyria compose, and settings.
2026-06-07 14:15:42 +01:00
## Magenta RealTime 2 (optional live layer)
On **Apple Silicon ** (Kraken), install [magenta-rt ](https://github.com/magenta/magenta-realtime ) for sub-second live generation:
``` bash
uv pip install "magenta-rt[mlx]"
mrt models init && mrt models download
mrt mlx generate --prompt "disco funk" --duration 4.0 --model= mrt2_small
```
Wire MRT2 as a bridge between tracks or as a live “bed” under the Lyria queue — PRs welcome.
2026-06-07 14:22:39 +01:00
## Repo
2026-06-07 14:15:42 +01:00
2026-06-07 14:22:39 +01:00
Public on Git Studio: **https://tinqs.com/tinqs/live-radio **
2026-06-07 14:15:42 +01:00
``` bash
2026-06-07 14:22:39 +01:00
git clone git@ssh.tinqs.com:tinqs/live-radio.git
cd live-radio
git lfs install && git lfs pull
2026-06-07 14:15:42 +01:00
```
2026-06-07 15:20:10 +01:00
### Public auto-play (Git Studio / tinqs.com)
**Share this link ** — opens in browser and starts shuffling saved tracks from Git LFS (no local server):
**https://tinqs.com/tinqs/live-radio/src/branch/main/gateway/index.html **
Click **Preview ** or the expand icon on that file in Git Studio. If the browser blocks autoplay, tap **Start Live Ozan Radio ** once.
After generating new tracks locally, refresh the public page:
``` powershell
python -m ozan_radio export-web
2026-06-07 16:38:52 +01:00
git add gateway / index . html gateway / winamp . html gateway / playlist . json
2026-06-07 15:20:10 +01:00
git commit -m " Update public radio playlist "
git push
```
New generations auto-update `gateway/index.html` when the local server saves a track (`queue.add` ).
Full dashboard (`gateway/player.html` or `http://127.0.0.1:8787/player` ) still needs the FastAPI server for DJ chat, Lyria compose, and settings.
2026-06-07 14:15:42 +01:00
## Agent usage
DeepSeek (Pi, Cursor, Claude Code) can operate the station via HTTP or the skill in `.cursor/skills/ozan-radio/SKILL.md` .
## License
Apache 2.0 — same spirit as Magenta RealTime 2 open weights.