import { NextRequest, NextResponse } from "next/server" import { execFileSync } from "child_process" import { existsSync, mkdirSync } from "fs" import { createHash } from "crypto" import { readFile, mkdir } from "fs/promises" import { join } from "path" const CACHE_DIR = join(process.cwd(), "public", "tts-cache") async function ensureCacheDir() { if (!existsSync(CACHE_DIR)) { await mkdir(CACHE_DIR, { recursive: true }) } } function getCachePath(text: string): string { const hash = createHash("md5").update(text).digest("hex") return join(CACHE_DIR, `${hash}.wav`) } export async function GET(request: NextRequest) { const text = request.nextUrl.searchParams.get("text") if (!text || text.length > 200) { return NextResponse.json({ error: "text param required (max 200 chars)" }, { status: 400 }) } // Sanitize: only allow alphanumeric, spaces, and common punctuation if (!/^[\w\sáéíóúñüÁÉÍÓÚÑÜ.,;:!¿?¡()\-]+$/.test(text)) { return NextResponse.json({ error: "Invalid text characters" }, { status: 400 }) } await ensureCacheDir() const cachePath = getCachePath(text) if (!existsSync(cachePath)) { try { execFileSync("espeak-ng", [ "-v", "es-mx", "-s", "140", "-p", "60", "-w", cachePath, text, ], { timeout: 10000 }) } catch { return NextResponse.json({ error: "TTS generation failed" }, { status: 500 }) } } const audioBuffer = await readFile(cachePath) return new NextResponse(audioBuffer, { headers: { "Content-Type": "audio/wav", "Content-Length": audioBuffer.length.toString(), "Cache-Control": "public, max-age=31536000, immutable", }, }) }