fix(audio): remove speed from TTS/cache/MinIO keys; fix presigned URL host rewrite
- Speed is no longer part of Kokoro generation, in-memory cache keys, or MinIO object keys — audio is always generated at 1.0 and playback speed is applied client-side via audioEl.playbackRate - presignAudio() now calls rewriteHost() so chapter audio URLs use the public MinIO endpoint (same as presignVoiceSample already did) - docker-compose.yml: rename MINIO_PUBLIC_ENDPOINT → PUBLIC_MINIO_PUBLIC_URL for the ui service so SvelteKit's $env/dynamic/public picks it up
This commit is contained in:
@@ -314,14 +314,12 @@
|
||||
async function tryPresign(
|
||||
targetSlug: string,
|
||||
targetChapter: number,
|
||||
targetVoice: string,
|
||||
targetSpeed: number
|
||||
targetVoice: string
|
||||
): Promise<string | null> {
|
||||
const params = new URLSearchParams({
|
||||
slug: targetSlug,
|
||||
n: String(targetChapter),
|
||||
voice: targetVoice,
|
||||
speed: String(targetSpeed)
|
||||
voice: targetVoice
|
||||
});
|
||||
const res = await fetch(`/api/presign/audio?${params}`);
|
||||
if (res.status === 404) return null;
|
||||
@@ -337,7 +335,6 @@
|
||||
if (audioStore.nextStatus !== 'none') return; // already running or done
|
||||
|
||||
const voice = audioStore.voice;
|
||||
const speed = audioStore.speed;
|
||||
|
||||
audioStore.nextStatus = 'prefetching';
|
||||
audioStore.nextChapterPrefetched = nextChapter;
|
||||
@@ -345,7 +342,7 @@
|
||||
|
||||
try {
|
||||
// Fast path: already generated
|
||||
const url = await tryPresign(slug, nextChapter, voice, speed);
|
||||
const url = await tryPresign(slug, nextChapter, voice);
|
||||
if (url) {
|
||||
stopNextProgress();
|
||||
audioStore.nextProgress = 100;
|
||||
@@ -358,14 +355,14 @@
|
||||
const res = await fetch(`/api/audio/${slug}/${nextChapter}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ voice, speed })
|
||||
body: JSON.stringify({ voice })
|
||||
});
|
||||
if (!res.ok) throw new Error(`Prefetch generation failed: HTTP ${res.status}`);
|
||||
|
||||
stopNextProgress();
|
||||
audioStore.nextProgress = 100;
|
||||
|
||||
const url2 = await tryPresign(slug, nextChapter, voice, speed);
|
||||
const url2 = await tryPresign(slug, nextChapter, voice);
|
||||
if (!url2) throw new Error('Prefetch: audio generated but presign returned 404');
|
||||
|
||||
audioStore.nextAudioUrl = url2;
|
||||
@@ -402,7 +399,6 @@
|
||||
|
||||
async function startPlayback() {
|
||||
const voice = audioStore.voice;
|
||||
const speed = audioStore.speed;
|
||||
|
||||
// Populate store metadata so layout + mini-bar have track info.
|
||||
audioStore.slug = slug;
|
||||
@@ -435,7 +431,7 @@
|
||||
}
|
||||
|
||||
// Fast path B: audio already in MinIO (presign check).
|
||||
const url = await tryPresign(slug, chapter, voice, speed);
|
||||
const url = await tryPresign(slug, chapter, voice);
|
||||
if (url) {
|
||||
audioStore.audioUrl = url;
|
||||
audioStore.status = 'ready';
|
||||
@@ -453,13 +449,13 @@
|
||||
const res = await fetch(`/api/audio/${slug}/${chapter}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ voice, speed })
|
||||
body: JSON.stringify({ voice })
|
||||
});
|
||||
if (!res.ok) throw new Error(`Generation failed: HTTP ${res.status}`);
|
||||
|
||||
await finishProgress();
|
||||
|
||||
const url2 = await tryPresign(slug, chapter, voice, speed);
|
||||
const url2 = await tryPresign(slug, chapter, voice);
|
||||
if (!url2) throw new Error('Audio generated but presign returned 404');
|
||||
audioStore.audioUrl = url2;
|
||||
audioStore.status = 'ready';
|
||||
|
||||
@@ -98,14 +98,12 @@ export async function presignVoiceSample(voice: string): Promise<string> {
|
||||
export async function presignAudio(
|
||||
slug: string,
|
||||
n: number,
|
||||
voice?: string,
|
||||
speed?: number
|
||||
voice?: string
|
||||
): Promise<string> {
|
||||
const params = new URLSearchParams();
|
||||
if (voice) params.set('voice', voice);
|
||||
if (speed) params.set('speed', String(speed));
|
||||
const qs = params.toString() ? `?${params.toString()}` : '';
|
||||
log.debug('minio', 'presigning audio', { slug, n, voice, speed });
|
||||
log.debug('minio', 'presigning audio', { slug, n, voice });
|
||||
let res: Response;
|
||||
try {
|
||||
res = await fetch(`${SCRAPER_URL}/api/presign/audio/${slug}/${n}${qs}`);
|
||||
@@ -126,7 +124,5 @@ export async function presignAudio(
|
||||
}
|
||||
const data = (await res.json()) as { url: string };
|
||||
log.debug('minio', 'presign audio ok', { slug, n });
|
||||
// The scraper now signs audio URLs with the public endpoint directly,
|
||||
// so no host rewrite is needed here.
|
||||
return data.url;
|
||||
return rewriteHost(data.url);
|
||||
}
|
||||
|
||||
@@ -10,9 +10,9 @@ const SCRAPER_URL = env.SCRAPER_API_URL ?? 'http://localhost:8080';
|
||||
* 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 }
|
||||
* Body: { voice?: string }
|
||||
* Response: { url: string, filename: string }
|
||||
* where `url` is a relative path to GET /api/audio/[slug]/[n]?voice=...&speed=...
|
||||
* where `url` is a relative path to GET /api/audio/[slug]/[n]?voice=...
|
||||
*/
|
||||
export const POST: RequestHandler = async ({ params, request }) => {
|
||||
const { slug, n } = params;
|
||||
@@ -21,7 +21,7 @@ export const POST: RequestHandler = async ({ params, request }) => {
|
||||
error(400, 'Invalid slug or chapter number');
|
||||
}
|
||||
|
||||
let body: { voice?: string; speed?: number } = {};
|
||||
let body: { voice?: string } = {};
|
||||
try {
|
||||
body = await request.json();
|
||||
} catch {
|
||||
@@ -45,10 +45,8 @@ export const POST: RequestHandler = async ({ params, request }) => {
|
||||
// The scraper returns a proxy URL pointing to /api/audio-proxy/... — we rewrite
|
||||
// it to our own /api/audio/[slug]/[n]?... 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({
|
||||
@@ -60,7 +58,7 @@ export const POST: RequestHandler = async ({ params, request }) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* GET /api/audio/[slug]/[n]?voice=...&speed=...
|
||||
* GET /api/audio/[slug]/[n]?voice=...
|
||||
* 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.
|
||||
*/
|
||||
@@ -72,10 +70,8 @@ export const GET: RequestHandler = async ({ params, url }) => {
|
||||
}
|
||||
|
||||
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()}`);
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { presignAudio } from '$lib/server/minio';
|
||||
import { log } from '$lib/server/logger';
|
||||
|
||||
/**
|
||||
* GET /api/presign/audio?slug=...&n=...&voice=...&speed=...
|
||||
* GET /api/presign/audio?slug=...&n=...&voice=...
|
||||
* Returns a presigned MinIO URL for the audio file so the browser
|
||||
* can stream it directly without going through the server.
|
||||
* Returns 404 when the audio has not been generated yet.
|
||||
@@ -13,14 +13,13 @@ export const GET: RequestHandler = async ({ url }) => {
|
||||
const slug = url.searchParams.get('slug');
|
||||
const n = parseInt(url.searchParams.get('n') ?? '', 10);
|
||||
const voice = url.searchParams.get('voice') ?? undefined;
|
||||
const speed = parseFloat(url.searchParams.get('speed') ?? '1') || 1;
|
||||
|
||||
if (!slug || !n || n < 1) {
|
||||
error(400, 'Missing slug or n');
|
||||
}
|
||||
|
||||
try {
|
||||
const presignedUrl = await presignAudio(slug, n, voice, speed);
|
||||
const presignedUrl = await presignAudio(slug, n, voice);
|
||||
return json({ url: presignedUrl });
|
||||
} catch (e) {
|
||||
const status = (e as { status?: number }).status;
|
||||
|
||||
Reference in New Issue
Block a user