Files
ozan 6e92841352 Fix generation JSON/Lyria errors, add Winamp player, and ship Echoes of the Sahel.
Harden DeepSeek JSON parsing with retry, pre-sanitize Lyria prompts, and instrumental fallback. Add pure HTML Winamp skin at /winamp with playlist export support.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-07 16:38:52 +01:00

205 lines
8.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Live Ozan Radio
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).
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 |
|-------|---------|------|
| DJ brain | DeepSeek (Tinqs proxy or BYOK) | Mood, prompts, chat, variety |
| Music engine | Google Lyria 3 Pro / Clip | Generate MP3 tracks |
| Taste | `settings.json` + `taste_seeds.json` | Profile from screenshots or manual edit — see below |
| Player | FastAPI + `gateway/player.html` | Stream queue, library, chat, dashboard |
| Live (optional) | Magenta RealTime 2 | Apple Silicon only — see below |
## 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).
## 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).
## Taste (`settings.json`)
Edit `settings.json` at the repo root, or let Cursor fill it from screenshots. The DJ reloads it on every generate and chat.
```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
}
}
```
**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.
The player settings panel PATCHes `playback` and `limits` via `/api/settings` and writes back to this file.
## 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 |
| `manifest.json` | gitignored | Runtime queue index |
Browse in the player under **Saved songs**, or `GET /api/songs`. After clone: `git lfs install` then `git lfs pull`.
## Quick start (Forge / Windows)
```powershell
cd live-ozan-radio
python -m venv .venv
.venv\Scripts\activate
pip install -e .
copy .env.example .env
# Fill GEMINI_API_KEY + DEEPSEEK_API_KEY
# Taste: docs/TASTE-FROM-SCREENSHOTS.md (Cursor + Spotify screenshots)
$env:DEEPSEEK_BASE_URL = "https://api.deepseek.com/v1"
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 |
| `DEEPSEEK_API_KEY` | Yes | Tinqs proxy token or DeepSeek direct |
| `DEEPSEEK_BASE_URL` | No | Default `https://api.deepseek.com/v1` |
| `LYRIA_MODEL` | No | `lyria-3-pro-preview` (default) or `lyria-3-clip-preview` |
| `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.
## API
| Method | Path | Description |
|--------|------|-------------|
| GET | `/api/now` | Current track metadata |
| GET | `/api/queue` | Full queue |
| GET | `/api/stats` | Dashboard: today's spend, quota, cost estimates |
| GET | `/api/settings` | Playback, limits, costs |
| PATCH | `/api/settings` | Update shuffle, daily cap, etc. |
| POST | `/api/generate` | DJ plans + Lyria renders next track |
| 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 |
| GET | `/stream/{file}` | MP3 stream |
| GET | `/player` | Full DJ dashboard |
| GET | `/winamp` | Winamp-style HTML player (streams `/stream/{file}` locally) |
## Winamp player (pure HTML)
Lightweight **Winamp 2.xstyle** 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.
## 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.
## Repo
Public on Git Studio: **https://tinqs.com/tinqs/live-radio**
```bash
git clone git@ssh.tinqs.com:tinqs/live-radio.git
cd live-radio
git lfs install && git lfs pull
```
### 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
git add gateway/index.html gateway/winamp.html gateway/playlist.json
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.
## 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.