www_site/app/tts-clone/route.ts

66 lines
2.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 });
}
}