Define techno-ethnic taste lane and notify when generation is ready.

Bonobo, Jamaica dub, Sahara, Mongolia overtone, and Urdu colour in settings and DJ prompts. Generate runs in background with polling, ready toast, optional browser notification, and autoplay of the new track.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-07 16:27:07 +01:00
parent e15b5e9f98
commit 5f90945b97
9 changed files with 308 additions and 186 deletions
+57 -6
View File
@@ -3,6 +3,7 @@ from __future__ import annotations
import asyncio
import json
import random
from datetime import datetime, timezone
from pathlib import Path
from fastapi import BackgroundTasks, FastAPI, HTTPException
@@ -35,7 +36,15 @@ app.add_middleware(
_config: Config | None = None
_queue: RadioQueue | None = None
_generating = False
_generation_state: dict = {"busy": False, "phase": None, "error": None, "track_title": None}
_generation_state: dict = {
"busy": False,
"phase": None,
"error": None,
"track_title": None,
"last_completed_at": None,
"last_completed_title": None,
"last_completed_id": None,
}
_chat = ChatStore()
@@ -101,14 +110,29 @@ def _play_library_entry(q: RadioQueue, cfg: Config, entry: dict) -> dict:
return {"status": "ok", "source": "library", "track": np.__dict__ if np else None}
def _set_generation(*, busy: bool, phase: str | None = None, error: str | None = None, title: str | None = None) -> None:
def _set_generation(
*,
busy: bool,
phase: str | None = None,
error: str | None = None,
title: str | None = None,
completed_id: str | None = None,
) -> None:
global _generation_state
_generation_state = {
state = {
"busy": busy,
"phase": phase,
"error": error,
"track_title": title,
"last_completed_at": _generation_state.get("last_completed_at"),
"last_completed_title": _generation_state.get("last_completed_title"),
"last_completed_id": _generation_state.get("last_completed_id"),
}
if completed_id and title:
state["last_completed_at"] = datetime.now(timezone.utc).isoformat()
state["last_completed_title"] = title
state["last_completed_id"] = completed_id
_generation_state = state
async def _compose_track(request: str | None = None, *, check_limit: bool = True) -> dict:
@@ -138,12 +162,17 @@ async def _compose_track(request: str | None = None, *, check_limit: bool = True
_set_generation(busy=True, phase="composing", title=plan.title)
track = LyriaEngine(cfg).generate(plan)
q.add(track)
q.play_id(track.plan.id)
rs = load_radio_settings(lyria_model=cfg.lyria_model)
cost = cost_per_track(cfg.lyria_model, rs.costs.__dict__)
record_generation(cfg.output_dir, cost, track.plan.id, track.plan.title)
np = q.now_playing()
_, budget = _can_generate_today(cfg)
_set_generation(busy=False)
_set_generation(
busy=False,
title=track.plan.title,
completed_id=track.plan.id,
)
return {
"status": "ok",
"source": "generated",
@@ -262,8 +291,30 @@ async def patch_settings(body: SettingsPatch) -> dict:
@app.post("/api/generate")
async def generate_track() -> dict:
return await _compose_track()
async def generate_track(background: BackgroundTasks) -> dict:
"""Start generation in background — poll /api/stats for progress and ready state."""
cfg = _get_config()
if _generating:
return {
"status": "busy",
"message": "Already generating a track",
"generation": _generation_state,
}
ok, budget = _can_generate_today(cfg)
if not ok:
return {
"status": "limit",
"message": f"Daily limit reached ({budget['max_per_day']} new songs)",
"budget": budget,
}
background.add_task(_background_compose, None)
return {
"status": "accepted",
"generating": True,
"message": "Generating — planning then Lyria compose (~3090s)",
"budget": budget,
"generation": _generation_state,
}
@app.post("/api/shuffle/next")