Add DJ curation metadata, public auto-play radio, and Lyria web controls.

Extensive per-track meta feeds DeepSeek planning. Caravan of the Night kept with electric guitar marked disliked. Sahara Saz remains gold standard. Gateway index.html auto-plays on tinqs.com.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-07 15:20:10 +01:00
parent 4b2003866d
commit b2aad43a44
25 changed files with 1869 additions and 75 deletions
+56
View File
@@ -1,6 +1,7 @@
from __future__ import annotations
import asyncio
import json
import random
from pathlib import Path
@@ -19,6 +20,8 @@ from ozan_radio.library import list_saved_songs
from ozan_radio.lyria import GeneratedTrack
from ozan_radio.radio_settings import load_radio_settings, save_radio_settings_patch
from ozan_radio.stats import cost_per_track, record_generation, today_stats
from ozan_radio.lyria_capabilities import probe_lyria_api, static_capabilities
from ozan_radio.settings import load_lyria_settings
from ozan_radio.taste import load_taste_seeds
app = FastAPI(title="Live Ozan Radio", version="0.1.0")
@@ -44,6 +47,7 @@ class SettingsPatch(BaseModel):
playback: dict | None = None
limits: dict | None = None
costs: dict | None = None
lyria: dict | None = None
def _can_generate_today(cfg: Config) -> tuple[bool, dict]:
@@ -175,6 +179,7 @@ async def root() -> dict:
"songs": "/api/songs",
"stats": "/api/stats",
"settings": "/api/settings",
"lyria": "/api/lyria",
"shuffle": "POST /api/shuffle/next",
"player": "/player",
},
@@ -212,6 +217,26 @@ async def dashboard_stats() -> dict:
return _dashboard_stats(cfg)
@app.get("/api/lyria")
async def lyria_capabilities() -> dict:
cfg = _get_config()
caps = static_capabilities()
api_status = probe_lyria_api(cfg.gemini_api_key)
active = load_lyria_settings().to_public_dict()
available = set(api_status.get("models_available") or [])
models = []
for m in caps["models"]:
entry = dict(m)
entry["available"] = not available or m["id"] in available
models.append(entry)
caps["models"] = models
return {
"capabilities": caps,
"active": active,
"api": api_status,
}
@app.get("/api/settings")
async def get_settings() -> dict:
cfg = _get_config()
@@ -310,6 +335,34 @@ async def chat_with_dj(body: ChatRequest, background: BackgroundTasks) -> dict:
return result
def _vocal_cues_path() -> Path:
return Path(__file__).resolve().parents[2] / "vocal_cues.json"
def _load_vocal_cues() -> dict:
path = _vocal_cues_path()
if not path.exists():
return {"tracks": {}}
try:
return json.loads(path.read_text(encoding="utf-8"))
except (json.JSONDecodeError, OSError):
return {"tracks": {}}
@app.get("/api/vocal-cues")
async def vocal_cues_all() -> dict:
data = _load_vocal_cues()
return {"tracks": list(data.get("tracks", {}).keys()), "count": len(data.get("tracks", {}))}
@app.get("/api/vocal-cues/{track_id}")
async def vocal_cues_for_track(track_id: str) -> dict:
cues = _load_vocal_cues().get("tracks", {}).get(track_id)
if not cues:
raise HTTPException(404, "No vocal cues for this track")
return {"track_id": track_id, **cues}
@app.get("/api/songs")
async def saved_songs() -> dict:
cfg = _get_config()
@@ -370,6 +423,9 @@ async def _background_compose(request: str | None = None) -> None:
result = await _compose_track(request)
if result.get("status") == "error":
_chat.add("dj", f"Couldn't finish that track: {result.get('message', 'unknown error')}")
elif result.get("status") == "ok" and result.get("track"):
t = result["track"]
_chat.add("dj", f"Fresh cut ready: {t.get('title', 'new track')} — hitting the stream now.")
async def _autofill_queue(target: int = 2) -> None: