www_site/app/tts-clone/route.ts

66 lines
2.7 KiB
TypeScript
Raw Permalink Normal View History

import { NextResponse } from "next/server";
export const runtime = "nodejs";
const formatMime: Record<string, string> = {
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 });
}
}