Fix Lyria silent failures and surface generation status in the player.

Robust candidate-part parsing, quota-aware errors, live composing feedback, and two new desert dub tracks in the library.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-07 14:33:16 +01:00
parent 41bb4d6b29
commit 6843ecd6b0
8 changed files with 195 additions and 17 deletions
+54 -4
View File
@@ -267,7 +267,10 @@
font-size: 0.75rem;
color: var(--muted);
text-align: center;
line-height: 1.4;
}
.status.generating { color: var(--accent); }
.status.error { color: #ff5c7a; }
.stack {
font-size: 0.7rem;
color: #555568;
@@ -518,6 +521,7 @@
let currentTrackId = null;
let radioSettings = { playback: { shuffle: true, mix_existing_and_new: true, new_song_chance: 0.35 } };
let savingSettings = false;
let lastGenError = null;
function fmtUsd(n) {
return '$' + Number(n || 0).toFixed(2);
@@ -539,6 +543,34 @@
loadLibrary();
}
function applyGenerationState(gen) {
if (!gen) return;
if (gen.busy) {
genBtn.disabled = true;
const title = gen.track_title ? ` "${gen.track_title}"` : '';
const phase = gen.phase === 'planning'
? `DeepSeek planning${title}`
: `Lyria composing${title}… (~30s)`;
statusEl.textContent = phase;
statusEl.classList.add('generating');
statusEl.classList.remove('error');
return;
}
genBtn.disabled = false;
statusEl.classList.remove('generating');
if (gen.error) {
statusEl.textContent = gen.error;
statusEl.classList.add('error');
if (gen.error !== lastGenError) {
lastGenError = gen.error;
addBubble('dj', `Couldn't finish that track: ${gen.error}`);
}
return;
}
lastGenError = null;
statusEl.classList.remove('error');
}
function updateDashboard(stats) {
if (!stats) return;
const today = stats.today || {};
@@ -552,6 +584,7 @@
radioSettings.playback = stats.playback;
syncSettingsUI(stats.playback);
}
applyGenerationState(stats.generation);
updateModeBadge();
}
@@ -568,11 +601,12 @@
skipBtn.textContent = shuffle ? 'Shuffle next' : 'Skip';
}
async function loadStats() {
async function loadStats(returnData = false) {
try {
const res = await fetch(`${API}/api/stats`);
const data = await res.json();
updateDashboard(data);
return returnData ? data : undefined;
} catch (_) {}
}
@@ -704,7 +738,8 @@
const data = await res.json();
if (data.reply) addBubble('dj', data.reply);
if (data.generating) {
statusEl.textContent = 'DJ is composing your request…';
statusEl.textContent = 'DeepSeek planning your request…';
statusEl.classList.add('generating');
pollForNewTrack();
}
} catch (_) {
@@ -722,10 +757,17 @@
clearInterval(pollTimer);
pollTimer = setInterval(async () => {
n += 1;
const stats = await loadStats(true);
if (stats?.generation?.busy) return;
if (stats?.generation?.error) {
clearInterval(pollTimer);
return;
}
await refreshNow();
await loadStats();
const now = await fetch(`${API}/api/now`).then(r => r.json()).catch(() => null);
if (now?.track?.track_id !== currentTrackId) clearInterval(pollTimer);
if (n > 60) clearInterval(pollTimer);
}, 5000);
}, 3000);
}
async function refreshNow() {
@@ -756,6 +798,10 @@
} else if (data.status === 'limit') {
statusEl.textContent = data.message || 'Daily limit reached';
updateDashboard({ today: data.budget, costs: radioSettings.costs });
} else if (data.status === 'error') {
statusEl.textContent = data.message || 'Generation failed';
statusEl.classList.add('error');
applyGenerationState(data.generation || { error: data.message });
} else if (data.status === 'ok') {
if (data.track) applyTrack(data.track, data.source || 'generated');
await loadStats();
@@ -780,8 +826,12 @@
statusEl.textContent = data.message || 'Daily limit — no more new songs today';
} else if (data.status === 'busy') {
statusEl.textContent = data.message;
statusEl.classList.add('generating');
pollForNewTrack();
return;
} else if (data.status === 'error') {
statusEl.textContent = data.message || 'Generation failed';
statusEl.classList.add('error');
} else if (data.status === 'ok' && data.track) {
applyTrack(data.track, data.source);
} else if (data.status === 'idle') {
Before
After