v2 #1
@@ -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();
|
||||
|
||||
92
ui/src/routes/api/audio/[slug]/[n]/+server.ts
Normal file
92
ui/src/routes/api/audio/[slug]/[n]/+server.ts
Normal file
@@ -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<typeof error>[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 <audio> element uses as its src.
|
||||
*/
|
||||
export const GET: RequestHandler = async ({ params, url }) => {
|
||||
const { slug, n } = params;
|
||||
const chapter = parseInt(n, 10);
|
||||
if (!slug || !chapter || chapter < 1) {
|
||||
error(400, 'Invalid slug or chapter number');
|
||||
}
|
||||
|
||||
const voice = url.searchParams.get('voice') ?? '';
|
||||
const speed = url.searchParams.get('speed') ?? '1';
|
||||
const qs = new URLSearchParams();
|
||||
if (voice) qs.set('voice', voice);
|
||||
qs.set('speed', speed);
|
||||
|
||||
const scraperRes = await fetch(`${SCRAPER_URL}/api/audio-proxy/${slug}/${chapter}?${qs.toString()}`);
|
||||
|
||||
if (!scraperRes.ok) {
|
||||
error(scraperRes.status as Parameters<typeof error>[0], 'Audio not found');
|
||||
}
|
||||
|
||||
// Stream the audio body through — preserve Content-Type and Content-Length.
|
||||
const headers = new Headers();
|
||||
headers.set('Content-Type', scraperRes.headers.get('Content-Type') ?? 'audio/mpeg');
|
||||
headers.set('Cache-Control', 'public, max-age=3600');
|
||||
const cl = scraperRes.headers.get('Content-Length');
|
||||
if (cl) headers.set('Content-Length', cl);
|
||||
|
||||
return new Response(scraperRes.body, { headers });
|
||||
};
|
||||
Reference in New Issue
Block a user