diff --git a/src/app/api/tts/route.ts b/src/app/api/tts/route.ts new file mode 100644 index 0000000..6bc87f9 --- /dev/null +++ b/src/app/api/tts/route.ts @@ -0,0 +1,74 @@ +import { NextRequest, NextResponse } from "next/server"; + +// POST /api/tts - Proxy TTS requests to avoid CORS issues +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const { engine, url, text, voice, speed } = body; + + if (!text) { + return NextResponse.json({ error: "Text is required" }, { status: 400 }); + } + + let ttsUrl: string; + let ttsBody: Record; + + if (engine === "edge") { + // Use Docker container name for internal networking, fallback to provided URL + const edgeHost = process.env.EDGE_TTS_URL || url || "http://edge-tts:5050"; + ttsUrl = `${edgeHost}/v1/audio/speech`; + ttsBody = { + model: "tts-1", + input: text, + voice: voice || "en-US-AvaNeural", + response_format: "mp3", + speed: speed || 1.0, + }; + } else if (engine === "kokoro") { + // Use Docker container name for internal networking, fallback to provided URL + const kokoroHost = process.env.KOKORO_TTS_URL || url || "http://kokoro-tts:8880"; + ttsUrl = `${kokoroHost}/v1/audio/speech`; + ttsBody = { + model: "kokoro", + input: text, + voice: voice || "af_bella", + response_format: "mp3", + speed: speed || 1.0, + }; + } else { + return NextResponse.json({ error: "Invalid engine" }, { status: 400 }); + } + + const response = await fetch(ttsUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(ttsBody), + }); + + if (!response.ok) { + const errorText = await response.text(); + console.error("TTS error:", response.status, errorText); + return NextResponse.json( + { error: `TTS service error: ${response.status}` }, + { status: 502 } + ); + } + + const audioBuffer = await response.arrayBuffer(); + + return new NextResponse(audioBuffer, { + headers: { + "Content-Type": "audio/mpeg", + "Content-Length": audioBuffer.byteLength.toString(), + }, + }); + } catch (error) { + console.error("TTS proxy error:", error); + return NextResponse.json( + { error: error instanceof Error ? error.message : "TTS proxy failed" }, + { status: 500 } + ); + } +} diff --git a/src/components/SettingsPanel.tsx b/src/components/SettingsPanel.tsx index 524ffe5..4cd67fb 100644 --- a/src/components/SettingsPanel.tsx +++ b/src/components/SettingsPanel.tsx @@ -269,50 +269,22 @@ export function SettingsPanel({ )} - {/* Edge TTS URL */} + {/* Edge TTS info */} {ttsSettings.engine === "edge" && (
-

- Edge TTS URL -

- - onTTSSettingsChange({ - ...ttsSettings, - edgeUrl: e.target.value, - }) - } - placeholder="http://localhost:5050" - className="w-full p-3 rounded-lg border border-[var(--border)] bg-[var(--background)] text-[var(--foreground)]" - /> -

- Uses Microsoft neural voices. Fast and natural sounding. +

+ Uses Microsoft neural voices via Edge TTS. Fast and natural sounding. + Requires the edge-tts Docker container running on the server.

)} - {/* Kokoro URL */} + {/* Kokoro info */} {ttsSettings.engine === "kokoro" && (
-

- Kokoro API URL -

- - onTTSSettingsChange({ - ...ttsSettings, - kokoroUrl: e.target.value, - }) - } - placeholder="http://localhost:8880" - className="w-full p-3 rounded-lg border border-[var(--border)] bg-[var(--background)] text-[var(--foreground)]" - /> -

- High quality but slower. Generates full audio before playing. +

+ High quality local TTS. Slower as it generates full audio before playing. + Requires the kokoro Docker container running on the server.

)} diff --git a/src/hooks/useTTS.ts b/src/hooks/useTTS.ts index b884d75..e680604 100644 --- a/src/hooks/useTTS.ts +++ b/src/hooks/useTTS.ts @@ -118,22 +118,24 @@ export function useTTS({ settings, text }: UseTTSOptions): UseTTSReturn { setIsLoading(true); try { - const response = await fetch(`${settings.edgeUrl}/v1/audio/speech`, { + // Use proxy to avoid CORS issues + const response = await fetch("/api/tts", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ - model: "tts-1", - input: textRef.current, + engine: "edge", + url: settings.edgeUrl, + text: textRef.current, voice: settings.voice || "en-US-AvaNeural", - response_format: "mp3", speed: settings.speed, }), }); if (!response.ok) { - throw new Error(`Edge TTS API error: ${response.status}`); + const error = await response.json().catch(() => ({})); + throw new Error(error.error || `TTS error: ${response.status}`); } const blob = await response.blob(); @@ -179,7 +181,7 @@ export function useTTS({ settings, text }: UseTTSOptions): UseTTSReturn { } catch (error) { console.error("Edge TTS error:", error); setIsLoading(false); - alert("Failed to connect to Edge TTS. Make sure it's running at " + settings.edgeUrl); + alert(error instanceof Error ? error.message : "Edge TTS failed"); } }, [settings.edgeUrl, settings.speed, settings.voice]); @@ -187,22 +189,24 @@ export function useTTS({ settings, text }: UseTTSOptions): UseTTSReturn { setIsLoading(true); try { - const response = await fetch(`${settings.kokoroUrl}/v1/audio/speech`, { + // Use proxy to avoid CORS issues + const response = await fetch("/api/tts", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ - model: "kokoro", - input: textRef.current, + engine: "kokoro", + url: settings.kokoroUrl, + text: textRef.current, voice: "af_bella", - response_format: "mp3", speed: settings.speed, }), }); if (!response.ok) { - throw new Error(`Kokoro API error: ${response.status}`); + const error = await response.json().catch(() => ({})); + throw new Error(error.error || `TTS error: ${response.status}`); } const blob = await response.blob(); @@ -249,7 +253,7 @@ export function useTTS({ settings, text }: UseTTSOptions): UseTTSReturn { } catch (error) { console.error("Kokoro TTS error:", error); setIsLoading(false); - alert("Failed to connect to Kokoro. Make sure it's running at " + settings.kokoroUrl); + alert(error instanceof Error ? error.message : "Kokoro TTS failed"); } }, [settings.kokoroUrl, settings.speed]);