import { NextResponse } from "next/server"; import { Communicate } from "edge-tts-ts"; export const runtime = "nodejs"; export const dynamic = "force-dynamic"; const DEFAULT_VOICE = process.env.EDGE_TTS_VOICE || "zh-CN-XiaoxiaoNeural"; const MAX_TEXT_LENGTH = 12000; type EdgeTtsRequest = { text?: unknown; voice?: unknown; }; const jsonError = (message: string, status: number) => NextResponse.json({ error: message }, { status }); export async function POST(request: Request) { let payload: EdgeTtsRequest; try { payload = (await request.json()) as EdgeTtsRequest; } catch { return jsonError("Invalid JSON body", 400); } const text = typeof payload.text === "string" ? payload.text.trim() : ""; if (!text) { return jsonError("text is required", 400); } if (text.length > MAX_TEXT_LENGTH) { return jsonError(`text must be ${MAX_TEXT_LENGTH} characters or fewer`, 413); } const voice = typeof payload.voice === "string" && payload.voice.trim() ? payload.voice.trim() : DEFAULT_VOICE; try { const communicate = new Communicate(text, { voice }); const chunks: Uint8Array[] = []; let byteLength = 0; for await (const chunk of communicate.stream()) { if (chunk.type !== "audio") continue; chunks.push(chunk.data); byteLength += chunk.data.byteLength; } if (byteLength === 0) { return jsonError("Edge TTS returned empty audio", 502); } const audio = new Uint8Array(byteLength); let offset = 0; for (const chunk of chunks) { audio.set(chunk, offset); offset += chunk.byteLength; } const audioBuffer = audio.buffer.slice( audio.byteOffset, audio.byteOffset + audio.byteLength, ); return new Response(audioBuffer, { headers: { "Content-Type": "audio/mpeg", "Cache-Control": "no-store", }, }); } catch (error) { console.error("[EdgeTTS] Failed to synthesize speech:", error); return jsonError("Failed to synthesize speech", 502); } }