v2 #1

Open
kamil wants to merge 231 commits from v2 into main
2 changed files with 97 additions and 5 deletions
Showing only changes of commit f95ad3ed29 - Show all commits

View File

@@ -55,7 +55,7 @@
status = 'generating';
errorMsg = '';
try {
const res = await fetch(`http://localhost:8080/ui/audio/${slug}/${chapter}`, {
const res = await fetch(`/api/audio/${slug}/${chapter}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ voice, speed })

View 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 });
};