# 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.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. ## 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.