feat(phase-1): persistent generation library

- Save every completed generation to SQLite (generation_store.py) with
  WAV and waveform peaks written to data/generations/<id>/
- Deferred DB write until success — cancelled/errored generations never
  touch the DB and never appear in the library
- Fixed cancel+regenerate IndexError: _reset_scheduler_caches() now
  directly zeros scheduler._step_index and running state in addition to
  clearing VibePod cache dicts; same explicit resets added in the fresh
  path of prepare_noise_scheduler as belt-and-suspenders
- Added /library page with GenerationCard, WaveformPreview, waveform
  fetch, play/pause, download, delete, pagination, empty + error states
- Added generation API routes (list, single, audio stream, waveform,
  delete) proxying to Python server
- Added Library nav link to Header with active state
- Persist script/speaker/CFG to localStorage so generate page state
  survives navigation
- Updated build plan: Phase 0+1 ticked off, better-sqlite3 moved to
  Phase 2, architectural note on Python owning all persistence
This commit is contained in:
2026-05-02 23:05:11 +01:00
parent 47e0c7e512
commit 13085166fb
13 changed files with 913 additions and 29 deletions
@@ -0,0 +1,23 @@
import { NextRequest, NextResponse } from "next/server";
export const dynamic = "force-dynamic";
const pythonUrl = () => process.env.VIBEVOICE_SERVER_URL ?? "http://localhost:8000";
export async function GET(_: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
try {
const res = await fetch(`${pythonUrl()}/generations/${id}/audio`);
if (!res.ok) return NextResponse.json({ error: "Audio not found" }, { status: res.status });
return new NextResponse(res.body, {
status: 200,
headers: {
"Content-Type": "audio/wav",
"Content-Disposition": `attachment; filename="${id}.wav"`,
"Cache-Control": "public, max-age=31536000, immutable",
},
});
} catch {
return NextResponse.json({ error: "Failed to reach server" }, { status: 502 });
}
}
+27
View File
@@ -0,0 +1,27 @@
import { NextRequest, NextResponse } from "next/server";
const pythonUrl = () => process.env.VIBEVOICE_SERVER_URL ?? "http://localhost:8000";
export async function GET(_: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
try {
const res = await fetch(`${pythonUrl()}/generations/${id}`, { cache: "no-store" });
if (!res.ok) return NextResponse.json({ error: "Not found" }, { status: res.status });
return NextResponse.json(await res.json());
} catch {
return NextResponse.json({ error: "Failed to reach server" }, { status: 502 });
}
}
export async function DELETE(_: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
try {
const res = await fetch(`${pythonUrl()}/generations/${id}`, { method: "DELETE" });
if (res.status === 404)
return NextResponse.json({ error: "Not found" }, { status: 404 });
if (!res.ok) return NextResponse.json({ error: "Upstream error" }, { status: res.status });
return new NextResponse(null, { status: 204 });
} catch {
return NextResponse.json({ error: "Failed to reach server" }, { status: 502 });
}
}
@@ -0,0 +1,16 @@
import { NextRequest, NextResponse } from "next/server";
const pythonUrl = () => process.env.VIBEVOICE_SERVER_URL ?? "http://localhost:8000";
export async function GET(_: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
try {
const res = await fetch(`${pythonUrl()}/generations/${id}/waveform`, { cache: "no-store" });
if (!res.ok) return NextResponse.json({ error: "Waveform not found" }, { status: res.status });
return NextResponse.json(await res.json(), {
headers: { "Cache-Control": "public, max-age=31536000, immutable" },
});
} catch {
return NextResponse.json({ error: "Failed to reach server" }, { status: 502 });
}
}
+19
View File
@@ -0,0 +1,19 @@
import { NextRequest, NextResponse } from "next/server";
const pythonUrl = () => process.env.VIBEVOICE_SERVER_URL ?? "http://localhost:8000";
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const limit = searchParams.get("limit") ?? "50";
const offset = searchParams.get("offset") ?? "0";
try {
const res = await fetch(`${pythonUrl()}/generations?limit=${limit}&offset=${offset}`, {
cache: "no-store",
});
if (!res.ok) return NextResponse.json({ error: "Upstream error" }, { status: res.status });
return NextResponse.json(await res.json());
} catch {
return NextResponse.json({ error: "Failed to reach server" }, { status: 502 });
}
}