mcp radio: the card keeps the station going itself (dj_next via inference chat, pre-cooked), no prompt-box messages; compact layout + explicit size reports
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014mBuCaQSgWwqhtzsmTBQuM
This commit is contained in:
1 parent
c42a6f5c87
commit
9cff04e694
1 file changed
+24
-1
+24
-1
@@ -26,6 +26,7 @@ const SHEET = {
|
||||
seconds: { type: "number", description: "Length, 30-120. Default 60.", default: 60 },
|
||||
dj_line: { type: "string", description: "What the DJ says before the track (1-3 sentences, spoken by TTS). Optional." },
|
||||
voice: { type: "string", description: "Kokoro voice for the DJ line, e.g. af_sky, af_nicole, am_adam, bf_emma. Default af_sky." },
|
||||
station: { type: "string", description: "The station's theme in a few words (e.g. 'city girl: late-night city pop'). The card keeps playing tracks on this theme by itself." },
|
||||
},
|
||||
required: ["title", "tags"],
|
||||
};
|
||||
@@ -36,13 +37,15 @@ const TOOLS = [
|
||||
inputSchema: { type: "object", properties: {} },
|
||||
_meta: { ui: { resourceUri: PROBE_URI }, "ui/resourceUri": PROBE_URI } },
|
||||
{ name: "dj_play", title: "Play a track on gandora",
|
||||
description: "You are the gandora DJ. Write one track sheet and put it on air: a player card appears in chat, renders the song (ACE-Step, ~15-60 s), speaks your dj_line, then plays. When a track ends with auto-DJ on, or the listener taps next or reacts, a message arrives starting with '[gandora]'; answer it by calling dj_play again with the next sheet, steering by their reactions. Keep chat text short between tracks.",
|
||||
description: "You are the gandora DJ. Write the opening track sheet (and a station theme) and put it on air: a player card appears in chat, renders the song (ACE-Step, ~15-60 s), speaks your dj_line, then plays. The card then keeps the station going on its own (a small DJ model writes the following sheets in your style, steered by the listener's Like / Not this / Next), so you do not need to call dj_play per track. Its play log and reactions reach you as model context. Call dj_play again only when the listener asks you for a change of direction or a new station.",
|
||||
inputSchema: SHEET,
|
||||
_meta: { ui: { resourceUri: RADIO_URI }, "ui/resourceUri": RADIO_URI } },
|
||||
{ name: "get_track", title: "Get a gandora test clip", description: "Probe UI only.",
|
||||
inputSchema: { type: "object", properties: { id: { type: "string", enum: Object.keys(TRACKS) } } }, _meta: APP_ONLY },
|
||||
{ name: "render_track", title: "Render a gandora track", description: "Radio UI only: renders a sheet to mp3 (base64).",
|
||||
inputSchema: SHEET, _meta: APP_ONLY },
|
||||
{ name: "dj_next", title: "Write the next sheet", description: "Radio UI only: the in-card DJ writes the next track sheet from the station and play history.",
|
||||
inputSchema: { type: "object", properties: { station: { type: "string" }, history: { type: "array" } } }, _meta: APP_ONLY },
|
||||
{ name: "dj_speak", title: "Speak a DJ line", description: "Radio UI only: TTS for the DJ line (base64 mp3).",
|
||||
inputSchema: { type: "object", properties: { text: { type: "string" }, voice: { type: "string" } }, required: ["text"] }, _meta: APP_ONLY },
|
||||
];
|
||||
@@ -89,6 +92,26 @@ async function callTool(name, a, env) {
|
||||
structuredContent: { mimeType: r.headers.get("content-type") || "audio/mpeg", data, renderer: r.headers.get("x-renderer") || "", cost: r.headers.get("x-cost") || "" } };
|
||||
} catch (e) { return fail(String(e.message || e)); }
|
||||
}
|
||||
if (name === "dj_next") {
|
||||
try {
|
||||
const hist = (Array.isArray(a.history) ? a.history : []).slice(-8);
|
||||
const sys = "You are the late-night DJ of gandora, a generative radio station. Write the NEXT track sheet as one JSON object and nothing else: " +
|
||||
"{\"title\",\"tags\",\"lyrics\",\"bpm\",\"key\",\"seconds\",\"dj_line\"}. tags: an ACE-Step style prompt (genre, instruments, mood, vocal type, production), comma-separated, under 300 chars. " +
|
||||
"lyrics: original, short ([verse] 4 lines, [chorus] 4 lines), never quote real songs; or empty for an instrumental now and then. key like 'A minor'. seconds 60-90. " +
|
||||
"dj_line: 1-2 warm, short sentences spoken before the track, can reference the last track. Stay on the station theme but vary tempo, key, texture and title; " +
|
||||
"lean toward what was liked, away from what was disliked or skipped.";
|
||||
const user = JSON.stringify({ station: a.station || "", recent: hist.map((h) => ({ title: h.title, tags: h.tags, bpm: h.bpm, key: h.key, reaction: h.reaction || "played" })) });
|
||||
const r = await inference(env, "/v1/chat/completions", { model: "claude-haiku-5-5", max_tokens: 900, temperature: 1,
|
||||
messages: [{ role: "system", content: sys }, { role: "user", content: user }] });
|
||||
const d = await r.json();
|
||||
const text = String(d.choices?.[0]?.message?.content || "");
|
||||
const m = text.match(/\{[\s\S]*\}/); if (!m) return fail("DJ wrote no sheet");
|
||||
const s = JSON.parse(m[0]);
|
||||
if (!s.title || !s.tags) return fail("DJ sheet missing title/tags");
|
||||
const sheet = { ...s, station: a.station || "", seconds: clamp(s.seconds, 30, 120, 75), seed: hash32(s.title + "|" + s.tags) };
|
||||
return { content: [{ type: "text", text: "next: " + sheet.title }], structuredContent: sheet };
|
||||
} catch (e) { return fail(String(e.message || e)); }
|
||||
}
|
||||
if (name === "dj_speak") {
|
||||
try {
|
||||
const r = await inference(env, "/v1/speech", { input: String(a.text || "").slice(0, 600), model: "tts-kokoro", voice: a.voice || "af_sky", response_format: "mp3" });
|
||||
|
||||
Reference in new issue
Block a user