from __future__ import annotations import json import uuid from dataclasses import dataclass import httpx from ozan_radio.config import Config 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, concise radio-host voice. You never play catalog music — only AI-generated tracks via Lyria 3. When the listener asks for a vibe, mood, genre, or "play something X", set action to "generate" and include a vibe_hint Lyria can use. Otherwise action is "none". Respond with JSON only: { "reply": "your on-air response (1-3 sentences)", "action": "none" or "generate", "vibe_hint": "optional — detailed direction for the next composed track" } """ DJ_SYSTEM = """You are the DJ for Live Ozan Radio — a personal AI station that never plays catalog music. Every track is generated fresh by Google Lyria 3 based on your prompts. Your job: 1. Read settings.json and taste_seeds.json (listener profile from screenshots or manual edits). 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. 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: { "title": "short track title", "mood": "one-line mood", "dj_line": "what you'd say on air (1 sentence)", "lyria_prompt": "detailed prompt for Lyria 3 Pro — structure, instruments, tempo, energy" } """ @dataclass class TrackPlan: id: str title: str mood: str dj_line: str lyria_prompt: str @dataclass class ChatReply: reply: str action: str vibe_hint: str class DeepSeekDJ: """DeepSeek orchestrates what plays next — taste in, Lyria prompt out.""" def __init__(self, config: Config) -> None: self._config = config def _taste_block(self) -> str: settings = load_settings() if settings: return 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() payload: dict = { "model": self._config.deepseek_model, "messages": messages, "temperature": 0.85, } if json_mode: payload["response_format"] = {"type": "json_object"} url = f"{self._config.deepseek_base_url.rstrip('/')}/chat/completions" async with httpx.AsyncClient(timeout=120) as client: resp = await client.post( url, headers={"Authorization": f"Bearer {self._config.deepseek_api_key}"}, json=payload, ) resp.raise_for_status() return resp.json()["choices"][0]["message"]["content"] async def chat( self, message: str, history: list[dict], *, now_playing: str | None = None, ) -> ChatReply: context = [f"Listener says: {message}"] if now_playing: context.append(f"Currently playing: {now_playing}") 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"]}) messages.append({"role": "user", "content": "\n".join(context)}) data = json.loads(await self._completion(messages, json_mode=True)) return ChatReply( reply=data.get("reply", "Copy that — stay tuned."), action=data.get("action", "none"), vibe_hint=data.get("vibe_hint", ""), ) async def plan_next( self, recent_titles: list[str], seeds: TasteSeeds | None = None, request: str | None = None, ) -> TrackPlan: self._config.require_deepseek() user_parts = [ "Plan the next generated track for Live Ozan Radio.", f"Station taste:\n{self._taste_block()}", ] if seeds: user_parts.append(f"Taste seeds:\n{seeds.summary}") if recent_titles: user_parts.append(f"Already played (avoid repeating): {', '.join(recent_titles[-5:])}") if request: user_parts.append(f"Listener request: {request}") messages = [ {"role": "system", "content": DJ_SYSTEM}, {"role": "user", "content": "\n".join(user_parts)}, ] data = json.loads(await self._completion(messages, json_mode=True)) return TrackPlan( id=str(uuid.uuid4())[:8], title=data.get("title", "Untitled Transmission"), mood=data.get("mood", ""), dj_line=data.get("dj_line", "Coming up — something new, just for you."), lyria_prompt=data.get("lyria_prompt", "An upbeat instrumental groove."), )