Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions apps/web/src/app/api/v1/music/[id]/cover/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { isJobId } from "@/lib/chat/images";
import { currentUser, infernet, json, unauthorized } from "@/lib/chat/server";

export const dynamic = "force-dynamic";

/** GET /api/v1/music/<id>/cover: the song's cover art (image/png) once ready. */
export async function GET(req: Request, { params }: { params: Promise<{ id: string }> }) {
const user = await currentUser(req);
if (!user) return unauthorized();
const { id } = await params;
if (!isJobId(id)) return json({ error: { message: "unknown music job", code: 404 } }, 404);
let upstream: Response;
try {
upstream = await infernet(`/api/chat/music/${id}/cover`, { signal: AbortSignal.timeout(20_000) });
} catch (e) {
return json({ error: { message: `network unreachable: ${e instanceof Error ? e.message : e}`, code: 502 } }, 502);
}
const type = upstream.headers.get("content-type") ?? "";
if (upstream.status === 200 && type.startsWith("image/png")) {
return new Response(upstream.body, { status: 200, headers: { "content-type": "image/png", "cache-control": "private, max-age=3600" } });
}
await upstream.body?.cancel().catch(() => {});
return json({ error: { message: "no cover art for this song (yet)", code: 404 } }, 404);
}
2 changes: 1 addition & 1 deletion apps/web/src/app/api/v1/music/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export async function GET(req: Request, { params }: { params: Promise<{ id: stri
// Infernet writes lyrics from a brief first; pass what it wrote along.
const pick = (k: string) => (typeof body[k] === "string" || typeof body[k] === "number" ? body[k] : undefined);
return json(
{ id, status: "pending", seconds: body.seconds ?? null, stage: pick("stage"), caption: pick("caption"), lyrics: pick("lyrics"), duration: pick("duration"), node: sanitizeNodeProgress(body.node) },
{ id, status: "pending", seconds: body.seconds ?? null, stage: pick("stage"), caption: pick("caption"), lyrics: pick("lyrics"), duration: pick("duration"), title: pick("title"), cover: pick("cover"), node: sanitizeNodeProgress(body.node) },
202,
{ "cache-control": "no-store" },
);
Expand Down
16 changes: 13 additions & 3 deletions apps/web/src/app/chat/chat-app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,8 @@ export function ChatApp({ error }: { error: string | null }) {
const start = await res.json().catch(() => ({}));
if (res.status !== 202 || !start.poll) throw new Error(start.error?.message ?? `HTTP ${res.status}`);
// Infernet may write lyrics from the brief first; keep what it wrote.
let written: { caption: string; lyrics: string } | null = null;
let written: { caption: string; lyrics: string; title?: string | null } | null = null;
let coverUrl: string | null = null;
const deadline = Date.now() + 45 * 60_000;
let done: Response | null = null;
while (!done) {
Expand All @@ -279,7 +280,15 @@ export function ChatApp({ error }: { error: string | null }) {
if (poll.status === 202) {
const p = await poll.json().catch(() => ({}));
const secs = Math.round((Date.now() - started) / 1000);
if (typeof p.caption === "string") written = { caption: p.caption, lyrics: typeof p.lyrics === "string" ? p.lyrics : "" };
if (typeof p.caption === "string") written = { caption: p.caption, lyrics: typeof p.lyrics === "string" ? p.lyrics : "", title: typeof p.title === "string" ? p.title : null };
// Cover art is drawn in parallel and usually lands before the song.
if (p.cover === "ready" && !coverUrl) {
const c = await fetch(`${start.poll}/cover`, { cache: "no-store", signal: ctrl.signal }).catch(() => null);
if (c?.ok) {
coverUrl = URL.createObjectURL(await c.blob());
setLast({ image: coverUrl });
}
}
const node = sanitizeNodeProgress(p.node);
const head =
p.stage === "writing"
Expand All @@ -299,7 +308,8 @@ export function ChatApp({ error }: { error: string | null }) {
const blob = await done.blob();
const url = URL.createObjectURL(blob);
setLast({
content: `[music: ${(written?.caption ?? music.prompt).slice(0, 300)}${written?.lyrics || music.lyrics ? " (with lyrics)" : ", instrumental"}] (${Math.round((Date.now() - started) / 1000)}s on a c0mpute worker)${written?.lyrics ? `\n\n${written.lyrics}` : ""}`,
...(coverUrl ? { image: coverUrl } : {}),
content: `${written?.title ? `"${written.title}" ` : ""}[music: ${(written?.caption ?? music.prompt).slice(0, 300)}${written?.lyrics || music.lyrics ? " (with lyrics)" : ", instrumental"}] (${Math.round((Date.now() - started) / 1000)}s on a c0mpute worker)${written?.lyrics ? `\n\n${written.lyrics}` : ""}`,
audio: url,
audioType: blob.type || "audio/mpeg",
});
Expand Down
Loading