import { NextResponse } from "next/server"; export const runtime = "nodejs"; const formatMime: Record = { wav: "audio/wav", mp3: "audio/mpeg", flac: "audio/flac", opus: "audio/opus", aac: "audio/aac", pcm: "application/octet-stream", }; // 真人声音配音(声音克隆): // 前端把「文本 + 客户声音样本(data URL)」发到这里,服务端再调用配置的声音克隆引擎,返回音频流。 // 生产环境需在服务端配置 TTS_CLONE_URL 指向 TTS 引擎的克隆接口; // 未配置时返回 501,前端会提示「真人配音引擎尚未接通,请先选择内置音色」。 export async function POST(request: Request) { let body: { text?: unknown; reference_audio?: unknown; response_format?: unknown; speed?: unknown }; try { body = await request.json(); } catch { return NextResponse.json({ error: { message: "请求体不是合法 JSON" } }, { status: 400 }); } const text = typeof body.text === "string" ? body.text.trim() : ""; const referenceAudio = typeof body.reference_audio === "string" ? body.reference_audio : ""; const format = typeof body.response_format === "string" && body.response_format ? body.response_format : "wav"; const speed = Number(body.speed) || 1; if (!text) return NextResponse.json({ error: { message: "请输入需要合成的文本" } }, { status: 400 }); if (!referenceAudio.startsWith("data:audio/")) { return NextResponse.json({ error: { message: "请先录制或上传声音样本" } }, { status: 400 }); } if (referenceAudio.length > 12 * 1024 * 1024) { return NextResponse.json({ error: { message: "声音样本过大,请控制在 8 MB 以内" } }, { status: 400 }); } const cloneUrl = process.env.TTS_CLONE_URL; if (!cloneUrl) { return NextResponse.json( { error: { message: "真人配音引擎尚未接通,请先选择内置音色" } }, { status: 501 }, ); } try { const upstream = await fetch(cloneUrl, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ input: text, reference_audio: referenceAudio, response_format: format, speed }), }); if (!upstream.ok) { const errBody = (await upstream.json().catch(() => ({}))) as { error?: { message?: string } }; throw new Error(errBody?.error?.message ?? `配音引擎返回 ${upstream.status}`); } const audio = Buffer.from(await upstream.arrayBuffer()); return new NextResponse(audio, { headers: { "Content-Type": formatMime[format] ?? "application/octet-stream" }, }); } catch (error) { const message = error instanceof Error ? error.message : "真人语音合成失败"; return NextResponse.json({ error: { message } }, { status: 502 }); } }