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:
2026-06-07 14:19:38 +01:00
parent b8ff25f370
commit feb8731366
6 changed files with 144 additions and 8 deletions
+4
View File
@@ -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`
+4
View File
@@ -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):
+53
View File
@@ -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
View File
@@ -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 12 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:
+64
View File
@@ -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
View File
@@ -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": [
{