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.
|
||||
- Respond in English.
|
||||
|
||||
## Taste
|
||||
|
||||
Edit `settings.json` — DJ + chat read it every request. Default: ethnic world dubtronica.
|
||||
|
||||
## Session start
|
||||
|
||||
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 |
|
||||
| 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
|
||||
|
||||
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.spotify import TasteProfile
|
||||
from ozan_radio.settings import ListenerSettings, load_settings
|
||||
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,
|
||||
@@ -32,8 +33,8 @@ Your job:
|
||||
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.
|
||||
4. Keep variety — don't repeat the same vibe twice in a row.
|
||||
5. Favor warm, groove-forward, slightly eclectic picks (Ozan's lane).
|
||||
6. West African / Sahel / desert blues / griot energy is on-brand (think Baaba Maal warmth).
|
||||
5. Follow settings.json taste profile when provided — it overrides generic defaults.
|
||||
6. Stay in the listener's lane unless they ask for something else in chat.
|
||||
|
||||
Respond with JSON only:
|
||||
{
|
||||
@@ -66,6 +67,12 @@ class DeepSeekDJ:
|
||||
|
||||
def __init__(self, config: Config) -> None:
|
||||
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:
|
||||
self._config.require_deepseek()
|
||||
@@ -98,7 +105,8 @@ class DeepSeekDJ:
|
||||
if 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:]:
|
||||
role = "assistant" if turn["role"] == "dj" else turn["role"]
|
||||
messages.append({"role": role, "content": turn["content"]})
|
||||
@@ -120,7 +128,10 @@ class DeepSeekDJ:
|
||||
) -> TrackPlan:
|
||||
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:
|
||||
user_parts.append(f"Spotify taste: {taste.summary}")
|
||||
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",
|
||||
"notes": "Manual taste seeds when Spotify API is not wired. Add tracks from your library — DJ reads vibe, never replays catalog.",
|
||||
"genres": [
|
||||
"ethnic world",
|
||||
"dubtronica",
|
||||
"world dub",
|
||||
"west african",
|
||||
"afro-folk",
|
||||
"desert blues",
|
||||
"griot",
|
||||
"world",
|
||||
"eclectic groove"
|
||||
"desert blues"
|
||||
],
|
||||
"tracks": [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user