diff --git a/ui/src/lib/components/AudioPlayer.svelte b/ui/src/lib/components/AudioPlayer.svelte index 187318c..992ea7a 100644 --- a/ui/src/lib/components/AudioPlayer.svelte +++ b/ui/src/lib/components/AudioPlayer.svelte @@ -55,11 +55,11 @@ status = 'generating'; errorMsg = ''; try { - const res = await fetch(`http://localhost:8080/ui/audio/${slug}/${chapter}`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ voice, speed }) - }); + const res = await fetch(`/api/audio/${slug}/${chapter}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ voice, speed }) + }); if (!res.ok) throw new Error(`Generation failed: ${res.status}`); // After generation, fetch the presigned URL await loadAudio(); diff --git a/ui/src/routes/api/audio/[slug]/[n]/+server.ts b/ui/src/routes/api/audio/[slug]/[n]/+server.ts new file mode 100644 index 0000000..c196bbe --- /dev/null +++ b/ui/src/routes/api/audio/[slug]/[n]/+server.ts @@ -0,0 +1,92 @@ +import { error } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; +import { env } from '$env/dynamic/private'; + +const SCRAPER_URL = env.SCRAPER_API_URL ?? 'http://localhost:8080'; + +/** + * POST /api/audio/[slug]/[n] + * Proxies the audio generation request to the scraper's /api/audio endpoint. + * Keeps the scraper URL server-side — the browser never needs to know it. + * + * Body: { voice?: string, speed?: number } + * Response: { url: string, filename: string } + * where `url` is a relative path to GET /api/audio/[slug]/[n]?voice=...&speed=... + */ +export const POST: RequestHandler = async ({ params, request }) => { + const { slug, n } = params; + const chapter = parseInt(n, 10); + if (!slug || !chapter || chapter < 1) { + error(400, 'Invalid slug or chapter number'); + } + + let body: { voice?: string; speed?: number } = {}; + try { + body = await request.json(); + } catch { + // empty body is fine — scraper will use defaults + } + + const scraperRes = await fetch(`${SCRAPER_URL}/api/audio/${slug}/${chapter}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body) + }); + + if (!scraperRes.ok) { + const text = await scraperRes.text().catch(() => ''); + error(scraperRes.status as Parameters[0], text || 'Audio generation failed'); + } + + const data = (await scraperRes.json()) as { url: string; filename: string }; + + // Rewrite the proxy URL from the scraper's /ui/audio-proxy/... to our own + // /api/audio/[slug]/[n]?voice=...&speed=... so the browser never calls the scraper directly. + const voice = body.voice ?? ''; + const speed = body.speed ?? 1.0; + const qs = new URLSearchParams(); + if (voice) qs.set('voice', voice); + qs.set('speed', String(speed)); + + return new Response( + JSON.stringify({ + url: `/api/audio/${slug}/${chapter}?${qs.toString()}`, + filename: data.filename + }), + { headers: { 'Content-Type': 'application/json' } } + ); +}; + +/** + * GET /api/audio/[slug]/[n]?voice=...&speed=... + * Proxies the audio stream from the scraper's /api/audio-proxy endpoint. + * This is the URL the browser's