/** * API Proxy Route: POST /api/generate * * This route proxies requests from the frontend to the FastAPI backend's /generate endpoint. * * Security Architecture: * The FastAPI backend is configured to bind only to localhost (127.0.0.1). This prevents * unauthenticated public access to the model inference engine. Next.js acts as a secure * proxy, allowing the frontend to interact with the backend while maintaining a * single public-facing origin. */ import { NextRequest, NextResponse } from "next/server"; export const dynamic = "force-dynamic"; export const runtime = "nodejs"; export async function POST(request: NextRequest) { const pythonServerUrl = process.env.VIBEVOICE_SERVER_URL ?? "http://localhost:8000"; try { const body = (await request.json()) as { text: string; speaker?: string; cfg_scale?: number; inference_steps?: number; }; if (!body.text?.trim()) { return NextResponse.json({ error: "Missing or empty text field" }, { status: 400 }); } const upstream = await fetch(`${pythonServerUrl}/generate`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ text: body.text.trim(), speaker: body.speaker ?? "carter", cfg_scale: body.cfg_scale ?? 1.5, inference_steps: body.inference_steps ?? 10, }), signal: request.signal, }); if (!upstream.ok) { const text = await upstream.text().catch(() => "Unknown error"); return NextResponse.json({ error: text }, { status: upstream.status }); } // Proxy the SSE stream through to the browser return new NextResponse(upstream.body, { status: 200, headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache, no-transform", Connection: "keep-alive", "X-Content-Type-Options": "nosniff", "X-Accel-Buffering": "no", }, }); } catch (err) { const message = err instanceof Error ? err.message : "Failed to connect to VibeVoice server"; return NextResponse.json({ error: message }, { status: 502 }); } }