Add settings.json taste profile — ethnic world dubtronica.
DJ and chat read listener preferences from settings.json on every request. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -8,6 +8,10 @@ Public repo under `tinqs/live-radio`. AI agents run the station — humans liste
|
|||||||
- **DeepSeek** plans mood + Lyria prompts. **Google Lyria 3** renders audio.
|
- **DeepSeek** plans mood + Lyria prompts. **Google Lyria 3** renders audio.
|
||||||
- Respond in English.
|
- Respond in English.
|
||||||
|
|
||||||
|
## Taste
|
||||||
|
|
||||||
|
Edit `settings.json` — DJ + chat read it every request. Default: ethnic world dubtronica.
|
||||||
|
|
||||||
## Session start
|
## Session start
|
||||||
|
|
||||||
1. Read `README.md`
|
1. Read `README.md`
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ Inspired by [Magenta RealTime 2](https://magenta.withgoogle.com/magenta-realtime
|
|||||||
| Player | FastAPI + `gateway/player.html` | Stream generated queue |
|
| Player | FastAPI + `gateway/player.html` | Stream generated queue |
|
||||||
| Live (optional) | Magenta RealTime 2 | Apple Silicon only — see below |
|
| Live (optional) | Magenta RealTime 2 | Apple Silicon only — see below |
|
||||||
|
|
||||||
|
## Taste (`settings.json`)
|
||||||
|
|
||||||
|
Edit `settings.json` at the repo root — the DJ reads it on every generate and chat. Default profile: **ethnic world dubtronica** (global roots + dub space + electronic groove).
|
||||||
|
|
||||||
## Saved songs
|
## Saved songs
|
||||||
|
|
||||||
Every track is written to `./songs/` and **committed via Git LFS** (audio) + plain git (metadata):
|
Every track is written to `./songs/` and **committed via Git LFS** (audio) + plain git (metadata):
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"listener": "ozan",
|
||||||
|
"station": "Live Ozan Radio",
|
||||||
|
"taste": {
|
||||||
|
"summary": "Ethnic world dubtronica — global roots, dub space, and electronic groove.",
|
||||||
|
"genres": [
|
||||||
|
"ethnic world",
|
||||||
|
"world dub",
|
||||||
|
"dubtronica",
|
||||||
|
"dub",
|
||||||
|
"global electronica",
|
||||||
|
"desert blues",
|
||||||
|
"afro-dub",
|
||||||
|
"middle eastern dub",
|
||||||
|
"balkan dub",
|
||||||
|
"trip-hop dub"
|
||||||
|
],
|
||||||
|
"mood": [
|
||||||
|
"hypnotic",
|
||||||
|
"warm",
|
||||||
|
"spacious",
|
||||||
|
"late-night",
|
||||||
|
"sunset caravan",
|
||||||
|
"meditative but danceable"
|
||||||
|
],
|
||||||
|
"instruments": [
|
||||||
|
"sub bass",
|
||||||
|
"dub delay and spring reverb",
|
||||||
|
"hand percussion",
|
||||||
|
"kora or oud",
|
||||||
|
"nose flute or melodica",
|
||||||
|
"tabla",
|
||||||
|
"djembe",
|
||||||
|
"muted guitar skank",
|
||||||
|
"analog warmth"
|
||||||
|
],
|
||||||
|
"tempo_bpm": [82, 108],
|
||||||
|
"references": "Thievery Corporation meets Sahel dub; Baaba Maal warmth over a stepper bassline; Khruangbin haze with Lee Scratch Perry space; ethnic samples woven into dubtronica, not EDM.",
|
||||||
|
"avoid": [
|
||||||
|
"big-room EDM drops",
|
||||||
|
"four-on-the-floor house",
|
||||||
|
"generic corporate lounge",
|
||||||
|
"overcompressed pop EDM"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"dj": {
|
||||||
|
"variety": true,
|
||||||
|
"default_length": "1-2 minutes"
|
||||||
|
},
|
||||||
|
"lyria": {
|
||||||
|
"prefer_instrumental": true
|
||||||
|
}
|
||||||
|
}
|
||||||
+15
-4
@@ -8,6 +8,7 @@ import httpx
|
|||||||
|
|
||||||
from ozan_radio.config import Config
|
from ozan_radio.config import Config
|
||||||
from ozan_radio.spotify import TasteProfile
|
from ozan_radio.spotify import TasteProfile
|
||||||
|
from ozan_radio.settings import ListenerSettings, load_settings
|
||||||
from ozan_radio.taste import TasteSeeds
|
from ozan_radio.taste import TasteSeeds
|
||||||
|
|
||||||
CHAT_SYSTEM = """You are the on-air DJ for Live Ozan Radio. Chat with the listener in a warm,
|
CHAT_SYSTEM = """You are the on-air DJ for Live Ozan Radio. Chat with the listener in a warm,
|
||||||
@@ -32,8 +33,8 @@ Your job:
|
|||||||
2. Pick a mood, tempo, and genre blend that feels like a natural next track.
|
2. Pick a mood, tempo, and genre blend that feels like a natural next track.
|
||||||
3. Write a Lyria prompt that produces a 1–2 minute instrumental or vocal track.
|
3. Write a Lyria prompt that produces a 1–2 minute instrumental or vocal track.
|
||||||
4. Keep variety — don't repeat the same vibe twice in a row.
|
4. Keep variety — don't repeat the same vibe twice in a row.
|
||||||
5. Favor warm, groove-forward, slightly eclectic picks (Ozan's lane).
|
5. Follow settings.json taste profile when provided — it overrides generic defaults.
|
||||||
6. West African / Sahel / desert blues / griot energy is on-brand (think Baaba Maal warmth).
|
6. Stay in the listener's lane unless they ask for something else in chat.
|
||||||
|
|
||||||
Respond with JSON only:
|
Respond with JSON only:
|
||||||
{
|
{
|
||||||
@@ -66,6 +67,12 @@ class DeepSeekDJ:
|
|||||||
|
|
||||||
def __init__(self, config: Config) -> None:
|
def __init__(self, config: Config) -> None:
|
||||||
self._config = config
|
self._config = config
|
||||||
|
self._settings = load_settings()
|
||||||
|
|
||||||
|
def _taste_block(self) -> str:
|
||||||
|
if self._settings:
|
||||||
|
return self._settings.dj_context()
|
||||||
|
return "No settings.json — freestyle eclectic world groove."
|
||||||
|
|
||||||
async def _completion(self, messages: list[dict], *, json_mode: bool = False) -> str:
|
async def _completion(self, messages: list[dict], *, json_mode: bool = False) -> str:
|
||||||
self._config.require_deepseek()
|
self._config.require_deepseek()
|
||||||
@@ -98,7 +105,8 @@ class DeepSeekDJ:
|
|||||||
if now_playing:
|
if now_playing:
|
||||||
context.append(f"Currently playing: {now_playing}")
|
context.append(f"Currently playing: {now_playing}")
|
||||||
|
|
||||||
messages = [{"role": "system", "content": CHAT_SYSTEM}]
|
chat_system = f"{CHAT_SYSTEM}\n\nStation taste:\n{self._taste_block()}"
|
||||||
|
messages = [{"role": "system", "content": chat_system}]
|
||||||
for turn in history[-8:]:
|
for turn in history[-8:]:
|
||||||
role = "assistant" if turn["role"] == "dj" else turn["role"]
|
role = "assistant" if turn["role"] == "dj" else turn["role"]
|
||||||
messages.append({"role": role, "content": turn["content"]})
|
messages.append({"role": role, "content": turn["content"]})
|
||||||
@@ -120,7 +128,10 @@ class DeepSeekDJ:
|
|||||||
) -> TrackPlan:
|
) -> TrackPlan:
|
||||||
self._config.require_deepseek()
|
self._config.require_deepseek()
|
||||||
|
|
||||||
user_parts = ["Plan the next generated track for Live Ozan Radio."]
|
user_parts = [
|
||||||
|
"Plan the next generated track for Live Ozan Radio.",
|
||||||
|
f"Station taste:\n{self._taste_block()}",
|
||||||
|
]
|
||||||
if taste:
|
if taste:
|
||||||
user_parts.append(f"Spotify taste: {taste.summary}")
|
user_parts.append(f"Spotify taste: {taste.summary}")
|
||||||
if taste.top_genres:
|
if taste.top_genres:
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ListenerSettings:
|
||||||
|
listener: str
|
||||||
|
summary: str
|
||||||
|
genres: list[str]
|
||||||
|
mood: list[str]
|
||||||
|
instruments: list[str]
|
||||||
|
tempo_bpm: list[int]
|
||||||
|
references: str
|
||||||
|
avoid: list[str]
|
||||||
|
prefer_instrumental: bool
|
||||||
|
|
||||||
|
def dj_context(self) -> str:
|
||||||
|
bpm = self.tempo_bpm
|
||||||
|
tempo = f"{bpm[0]}-{bpm[1]} BPM" if len(bpm) >= 2 else "mid tempo"
|
||||||
|
lines = [
|
||||||
|
f"Listener taste ({self.listener}): {self.summary}",
|
||||||
|
f"Genres: {', '.join(self.genres)}.",
|
||||||
|
f"Mood: {', '.join(self.mood)}.",
|
||||||
|
f"Instruments / production: {', '.join(self.instruments)}.",
|
||||||
|
f"Tempo: {tempo}.",
|
||||||
|
f"References: {self.references}",
|
||||||
|
]
|
||||||
|
if self.avoid:
|
||||||
|
lines.append(f"Avoid: {', '.join(self.avoid)}.")
|
||||||
|
if self.prefer_instrumental:
|
||||||
|
lines.append("Default to instrumental unless vocals are requested.")
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def load_settings(repo_root: Path | None = None) -> ListenerSettings | None:
|
||||||
|
root = repo_root or Path(__file__).resolve().parents[2]
|
||||||
|
path = root / "settings.json"
|
||||||
|
if not path.exists():
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
except (json.JSONDecodeError, OSError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
taste = data.get("taste", {})
|
||||||
|
lyria = data.get("lyria", {})
|
||||||
|
if not taste.get("summary") and not taste.get("genres"):
|
||||||
|
return None
|
||||||
|
|
||||||
|
return ListenerSettings(
|
||||||
|
listener=data.get("listener", "listener"),
|
||||||
|
summary=taste.get("summary", ""),
|
||||||
|
genres=taste.get("genres", []),
|
||||||
|
mood=taste.get("mood", []),
|
||||||
|
instruments=taste.get("instruments", []),
|
||||||
|
tempo_bpm=taste.get("tempo_bpm", []),
|
||||||
|
references=taste.get("references", ""),
|
||||||
|
avoid=taste.get("avoid", []),
|
||||||
|
prefer_instrumental=lyria.get("prefer_instrumental", True),
|
||||||
|
)
|
||||||
+4
-4
@@ -2,12 +2,12 @@
|
|||||||
"listener": "ozan",
|
"listener": "ozan",
|
||||||
"notes": "Manual taste seeds when Spotify API is not wired. Add tracks from your library — DJ reads vibe, never replays catalog.",
|
"notes": "Manual taste seeds when Spotify API is not wired. Add tracks from your library — DJ reads vibe, never replays catalog.",
|
||||||
"genres": [
|
"genres": [
|
||||||
|
"ethnic world",
|
||||||
|
"dubtronica",
|
||||||
|
"world dub",
|
||||||
"west african",
|
"west african",
|
||||||
"afro-folk",
|
"afro-folk",
|
||||||
"desert blues",
|
"desert blues"
|
||||||
"griot",
|
|
||||||
"world",
|
|
||||||
"eclectic groove"
|
|
||||||
],
|
],
|
||||||
"tracks": [
|
"tracks": [
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user